diff --git a/examples/Anomaly_Detection/Anomaly_Detection.ino b/examples/Anomaly_Detection/Anomaly_Detection.ino
new file mode 100644
index 00000000..c5aafe1f
--- /dev/null
+++ b/examples/Anomaly_Detection/Anomaly_Detection.ino
@@ -0,0 +1,79 @@
+/**
+ * Anomaly detection
+ * Detect when the frame changes by a reasonable amount
+ *
+ * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = DEBUG"
+ * to turn on debug messages
+ */
+#include <eloquent_esp32cam.h>
+#include <eloquent_esp32cam/anomaly/detection.h>
+
+using eloq::camera;
+using eloq::anomaly::detection;
+
+/**
+ *
+ */
+void setup() {
+    delay(3000);
+    Serial.begin(115200);
+    Serial.println("___ANOMALY DETECTION___");
+
+    // camera settings
+    // replace with your own model!
+    camera.pinout.xiao();
+    camera.brownout.disable();
+    camera.resolution.vga();
+    camera.quality.high();
+
+    // configure anomaly detection
+    detection.skip(4);
+    // the higher the stride, the faster the detection
+    // the higher the stride, the less the granularity
+    detection.stride(1);
+    // the higher the threshold, the less the sensitivity
+    // (at pixel level)
+    detection.threshold(5);
+    // the higher the detectionRatio, the less the sensitivity
+    // (at image level, from 0 to 1)
+    detection.detectionRatio(0.5);
+    // the higher the referenceRatio, the more the reference image can change over time
+    // (at image level, from 0 to 1)
+    detection.referenceRatio(0.2);
+    // optionally, you can enable rate limiting (aka debounce)
+    // anomaly won't trigger more often than the specified frequency
+    //detection.rate.atMostOnceEvery(5).seconds();
+
+    // init camera
+    while (!camera.begin().isOk())
+        Serial.println(camera.exception.toString());
+
+    Serial.println("Camera OK");
+    Serial.println("Awaiting anomaly...");
+}
+
+/**
+ *
+ */
+void loop() {
+
+    // Don't run more often than the time for an anomaly to come into view as the reference image can 'drift' away from 'normal'
+    delay(1000);
+    // capture picture
+    if (!camera.capture().isOk()) {
+        Serial.println(camera.exception.toString());
+        return;
+    }
+    
+    // run anomaly detection
+    if (!detection.run().isOk()) {
+        Serial.println(detection.exception.toString());
+        return;
+    }
+
+    // on anomaly, perform action
+    if (detection.triggered()) {
+        Serial.print("Anomaly detected: "); Serial.println(detection.movingRatio);
+    }
+}
+
diff --git a/examples/Anomaly_Detection_Higher_Resolution/Anomaly_Detection_Higher_Resolution.ino b/examples/Anomaly_Detection_Higher_Resolution/Anomaly_Detection_Higher_Resolution.ino
new file mode 100644
index 00000000..9afa0853
--- /dev/null
+++ b/examples/Anomaly_Detection_Higher_Resolution/Anomaly_Detection_Higher_Resolution.ino
@@ -0,0 +1,100 @@
+/**
+ * Run anomaly detection at low resolution.
+ * On anomaly, capture frame at higher resolution
+ * for SD storage.
+ *
+ * BE SURE TO SET "TOOLS > CORE DEBUG LEVEL = INFO"
+ * to turn on debug messages
+ */
+#include <eloquent_esp32cam.h>
+#include <eloquent_esp32cam/anomaly/detection.h>
+
+using eloq::camera;
+using eloq::anomaly::detection;
+
+
+/**
+ *
+ */
+void setup() {
+    delay(3000);
+    Serial.begin(115200);
+    Serial.println("___ANOMALY DETECTION + SWITCH RESOLUTION___");
+
+    // camera settings
+    // replace with your own model!
+    camera.pinout.freenove_s3();
+    camera.brownout.disable();
+    camera.resolution.vga();
+    camera.quality.high();
+
+    // see example of anomaly detection for config values
+    detection.skip(5);
+    detection.stride(1);
+    detection.threshold(5);
+    // the higher the detectionRatio, the less the sensitivity
+    // (at image level, from 0 to 1)
+    detection.detectionRatio(0.5);
+    // the higher the referenceRatio, the more the reference image can change over time
+    // (at image level, from 0 to 1)
+    detection.referenceRatio(0.2);
+
+    // init camera
+    while (!camera.begin().isOk())
+        Serial.println(camera.exception.toString());
+
+    Serial.println("Camera OK");
+    Serial.println("Awaiting for anomaly...");
+}
+
+/**
+ *
+ */
+void loop() {
+    // Don't run more often than the time for an anomaly to come into view as the reference image can 'drift' away from 'normal'
+    delay(1000);
+    // capture picture
+    if (!camera.capture().isOk()) {
+        Serial.println(camera.exception.toString());
+        return;
+    }
+
+    // run anomaly detection
+    if (!detection.run().isOk()) {
+        Serial.println(detection.exception.toString());
+        return;
+    }
+
+    // on anomaly, perform action
+    if (detection.triggered()) {
+        Serial.printf(
+          "Anomaly of %.2f detected on frame of size %dx%d (%d bytes)\n",
+		  detection.movingRatio,
+          camera.resolution.getWidth(),
+          camera.resolution.getHeight(),
+          camera.getSizeInBytes()
+        );
+
+        Serial.println("Taking photo of anomaly at higher resolution");
+
+        camera.resolution.at(FRAMESIZE_UXGA, []() {
+          Serial.printf(
+            "Switched to higher resolution: %dx%d. It took %d ms to switch\n",
+            camera.resolution.getWidth(),
+            camera.resolution.getHeight(),
+            camera.resolution.benchmark.millis()
+          );
+
+          camera.capture();
+
+          Serial.printf(
+            "Frame size is now %d bytes\n", 
+            camera.getSizeInBytes()
+          );
+
+          // save to SD...
+        });
+
+        Serial.println("Resolution switched back to VGA");
+    }
+}
\ No newline at end of file
diff --git a/src/eloquent_esp32cam/anomaly/daemon.h b/src/eloquent_esp32cam/anomaly/daemon.h
new file mode 100644
index 00000000..afbb8cb8
--- /dev/null
+++ b/src/eloquent_esp32cam/anomaly/daemon.h
@@ -0,0 +1,91 @@
+#ifndef ELOQUENT_ESP32CAM_ANOMALY_DAEMON_H
+#define ELOQUENT_ESP32CAM_ANOMALY_DAEMON_H
+
+#include <functional>
+#include "../camera/camera.h"
+#include "../extra/esp32/multiprocessing/thread.h"
+
+using eloq::camera;
+using Eloquent::Extra::Esp32::Multiprocessing::Thread;
+using OnAnomalyCallback = std::function<void(void)>;
+
+
+namespace Eloquent {
+    namespace Esp32cam {
+        namespace Anomaly {
+            /**
+             * Run anomaly detection in a task
+             * 
+             * @class Daemon
+             * @author jksemple
+             * @date 11/07/2024
+             * @file daemon.h
+             * @brief 
+             */
+             template<typename T>
+            class Daemon {
+            public:
+                Thread thread;
+                
+                /**
+                 * Constructor
+                 *
+                 * @brief 
+                 */
+                Daemon(T* detection) : 
+                    thread("AnomalyDetection"),
+                    _detection(detection) {
+                    
+                }
+            
+                /**
+                 * Run function when a difference from 'normal' is detected
+                 * 
+                 * @brief 
+                 * @param callback
+                 */
+                void onAnomaly(OnAnomalyCallback callback) {
+                    _onAnomaly = callback;
+                }
+                
+                /**
+                 * Start anomaly detection in background
+                 * 
+                 * @brief 
+                 */
+                void start() {
+                    thread
+                        .withArgs((void*) this)
+                        .withStackSize(5000)
+                        .run([](void *args) {
+                            Daemon *self = (Daemon*) args;
+                            
+                            delay(3000);
+                            
+                            while (true) {
+                                yield();
+                                delay(1);
+                                
+                                if (!camera.capture().isOk())
+                                    continue;
+
+                                if (!self->_detection->run().isOk())
+                                    continue;
+                                    
+                                if (!self->_detection->triggered())
+                                    continue;
+                                    
+                                self->_onAnomaly();
+                            }
+                        });
+                }
+                
+            protected:
+                T *_detection;
+                OnAnomalyCallback _onAnomaly;
+            };
+        }
+    }
+}
+
+#endif
diff --git a/src/eloquent_esp32cam/anomaly/detection.h b/src/eloquent_esp32cam/anomaly/detection.h
new file mode 100644
index 00000000..c6971297
--- /dev/null
+++ b/src/eloquent_esp32cam/anomaly/detection.h
@@ -0,0 +1,214 @@
+#ifndef ELOQUENT_ESP32CAM_ANOMALY_DETECTION
+#define ELOQUENT_ESP32CAM_ANOMALY_DETECTION
+
+#include <dl_image.hpp>
+#include "../extra/exception.h"
+#include "../extra/time/benchmark.h"
+#include "../extra/time/rate_limit.h"
+#include "../extra/pubsub.h"
+#include "./daemon.h"
+
+using eloq::camera;
+using Eloquent::Error::Exception;
+using Eloquent::Extra::Time::Benchmark;
+using Eloquent::Extra::Time::RateLimit;
+#if defined(ELOQUENT_EXTRA_PUBSUB_H)
+using Eloquent::Extra::PubSub;
+#endif
+
+
+namespace Eloquent {
+    namespace Esp32cam {
+        namespace Anomaly {
+            /**
+             * Detect anomaly using "fast" algorithm
+             */
+            class Detection {
+                public:
+                    float movingRatio;
+                    Exception exception;
+                    Benchmark benchmark;
+                    RateLimit rate;
+                    Daemon<Detection> daemon;
+                    #if defined(ELOQUENT_EXTRA_PUBSUB_H)
+                    PubSub<Detection> mqtt;
+                    #endif
+
+                    /**
+                     * 
+                     */
+                    Detection() :
+                        _stride(4),
+                        _threshold(5),
+                        _lowerDetectionRatio(0.2),
+						_upperDetectionRatio(0.5),
+						_referenceRatio(0.05),
+                        _reference(NULL),
+                        _skip(5),
+                        movingRatio(0),
+                        daemon(this),
+                        #if defined(ELOQUENT_EXTRA_PUBSUB_H)
+                        mqtt(this),
+                        #endif
+                        exception("AnomalyDetection") {
+
+                        }
+
+                    /**
+                     * Set detection stride.
+                     * The greater the value, the less accurate.
+                     * The greater the value, the faster.
+                    */
+                    void stride(uint8_t stride) {
+                        _stride = stride;
+                    }
+
+                    /**
+                     * Set detection sensitivity (pixel level).
+                     * The greater the value, the less sensitive the detection.
+                     */
+                    void threshold(uint8_t threshold) {
+                        _threshold = threshold;
+                    }
+
+                    /**
+                     * @brief Skip first frames (to avoid false detection)
+                     * @param skip
+                     */
+                    void skip(uint8_t skip) {
+                        _skip = skip;
+                    }
+
+                    /**
+                     * Set reference image sensitivity (image level).
+                     * The greater the value, the more the reference image can vary over time.
+                     */
+                    void referenceRatio(float ratio) {
+                        _referenceRatio = ratio;
+                    }
+
+                    /**
+                     * Set detection sensitivity (image level).
+                     * The greater the value, the less sensitive the detection.
+                     */
+                    void lowerDetectionRatio(float ratio) {
+                        _lowerDetectionRatio = ratio;
+                    }
+
+                    /**
+                     * Set maximum detection sensitivity (image level).
+                     * This protects against false detections when the light level across the whole image changes
+					 * Useful when you know objects being detected will never fill more than a fraction of the image
+                     */
+                    void upperDetectionRatio(float ratio) {
+                        _upperDetectionRatio = ratio;
+                    }
+                    /**
+                     * Test if anomaly triggered
+                     */
+                    inline bool triggered() {
+                        return movingRatio >= _lowerDetectionRatio && movingRatio <= _upperDetectionRatio;
+                    }
+
+					Exception& setReference() {
+                        // convert JPEG to RGB565
+                        if (!camera.rgb565.convert().isOk())
+                            return camera.rgb565.exception;
+
+						if (_reference == NULL) {
+							_reference = (uint16_t*) ps_malloc(camera.rgb565.length * sizeof(uint16_t));
+						}
+                        copy(camera.rgb565);
+
+                        return exception.clear();
+					}
+                    /**
+                     * 
+                     */
+                    Exception& run() {
+                        // skip fre first frames
+                        if (_skip > 0 && _skip-- > 0)
+                            return exception.set(String("Still ") + _skip + " frames to skip...");
+
+                        // convert JPEG to RGB565
+                        // this reduces the frame to 1/8th
+                        if (!camera.rgb565.convert().isOk())
+                            return camera.rgb565.exception;
+
+                        // first frame, only copy frame to prev
+                        if (_reference == NULL) {
+                            _reference = (uint16_t*) malloc(camera.rgb565.length * sizeof(uint16_t));
+                            copy(camera.rgb565);
+
+                            return exception.set("First frame, can't detect anomaly").soft();
+                        }
+
+                        benchmark.timeit([this]() {
+                            int movingPoints = dl::image::get_moving_point_number(
+                                camera.rgb565.data, 
+                                _reference, 
+                                camera.rgb565.height, 
+                                camera.rgb565.width, 
+                                _stride, 
+                                _threshold
+                            );
+
+                            movingRatio = ((float) movingPoints) / camera.rgb565.length * _stride * _stride;
+							if (movingRatio < _referenceRatio) {
+								ESP_LOGD("AnomalyDetection", "Replacing reference frame - referenceRatio = %.2f", movingRatio);
+								copy(camera.rgb565);
+							}
+                        });
+                        ESP_LOGD("AnomalyDetection", "moving points ratio: %.2f", movingRatio);
+
+                        // rate limit
+                        if (triggered() && !rate)
+                            return exception.set(rate.getRetryInMessage()).soft();
+
+                        if (triggered())
+                            rate.touch();
+
+                        return exception.clear();
+                    }
+                    /**
+                     * @brief Convert to JSON
+                     */
+                    String toJSON() {
+                        return String("{\"anomaly\":") + (triggered() ? "true" : "false") + "}";
+                    }
+
+                    /**
+                     * @brief Test if an MQTT message should be published
+                     */
+                    bool shouldPub() {
+                        return triggered();
+                    }
+
+                protected:
+                    uint8_t _skip;
+                    uint16_t *_reference;
+                    uint8_t _stride;
+                    uint8_t _threshold;
+                    float _lowerDetectionRatio;
+					float _upperDetectionRatio;
+					float _referenceRatio;
+
+                    /**
+                     * 
+                     */
+                    template<typename Frame>
+                    void copy(Frame frame) {
+                        memcpy((uint8_t*) _reference, (uint8_t*) frame.data, frame.length * sizeof(uint16_t));
+                    }
+            };
+        }
+    }
+}
+
+namespace eloq {
+    namespace anomaly {
+        static Eloquent::Esp32cam::Anomaly::Detection detection;
+    }
+}
+
+#endif
diff --git a/src/eloquent_esp32cam/anomaly/roi_detection.h b/src/eloquent_esp32cam/anomaly/roi_detection.h
new file mode 100644
index 00000000..412d9b45
--- /dev/null
+++ b/src/eloquent_esp32cam/anomaly/roi_detection.h
@@ -0,0 +1,158 @@
+#ifndef ELOQUENT_ESP32CAM_ANOMALY_ROI_DETECTION
+#define ELOQUENT_ESP32CAM_ANOMALY_ROI_DETECTION
+
+#include "./detection.h"
+
+namespace Eloquent {
+    namespace Esp32cam {
+        namespace Anomaly {
+            /**
+             * Perform anomaly detection on Region of Interest
+             */
+            class RoI : public Detection {
+                public:
+                    struct {
+                        uint16_t x;
+                        uint16_t y;
+                        uint16_t width;
+                        uint16_t height;
+                        uint16_t x1;
+                        uint16_t x2;
+                        uint16_t y1;
+                        uint16_t y2;
+                    } coords;
+
+                    /**
+                     * 
+                     */
+                    RoI() :
+                        _x(0),
+                        _y(0),
+                        _w(0),
+                        _h(0) {
+
+                        }
+
+                    /**
+                     * Set x coordinate (top-left corner)
+                     */
+                    void x(float x) {
+                        _x = x;
+                    }
+
+                    /**
+                     * Set y coordinate (top-left corner)
+                     */
+                    void y(float y) {
+                        _y = y;
+                    }
+
+                    /**
+                     * Set width of RoI
+                     */
+                    void width(float width) {
+                        _w = width;
+                    }
+
+                    /**
+                     * Set height of RoI
+                     */
+                    void height(float height) {
+                        _h = height;
+                    }
+
+                    /**
+                     * 
+                     */
+                    void updateCoords(uint16_t width, uint16_t height) {
+                        coords.x = max<int>(0, _x < 1 ? _x * width : _x);
+                        coords.y = max<int>(0, _y < 1 ? _y * height : _y);
+                        coords.width = min<int>(width - coords.x, _w < 1 ? _w * width : _w);
+                        coords.height = min<int>(height - coords.y, _h < 1 ? _h * height : _h);
+                        coords.x1 = coords.x;
+                        coords.y1 = coords.y;
+                        coords.x2 = coords.x + coords.width;
+                        coords.y2 = coords.y + coords.height;
+                    }
+
+                    /**
+                     * Detect anomaly
+                     */
+                    template<typename Frame>
+                    Exception& update(Frame& frame) {
+                        if (!_w || !_h)
+                            return exception.set("You MUST set a width and height for the RoI");
+
+                        if (_reference == NULL) {
+                            _reference = (uint8_t*) ps_malloc(_w * _h * sizeof(uint16_t));
+                            _roi  = (uint8_t*) ps_malloc(_w * _h * sizeof(uint16_t));
+                            copy(frame, _reference);
+
+                            return exception.set("First frame, can't detect anomaly").soft();
+                        }
+
+                        updateCoords(frame.width, frame.height);
+
+                        benchmark.timeit([this, &frame]() {
+                            copy(frame, _roi);
+
+                            int movingPoints = dl::image::get_moving_point_number((uint16_t *) _roi, (uint16_t*) _reference, coords.height, coords.width, _stride, _threshold);
+                            movingRatio = ((float) movingPoints) / sizeof(_roi) * _stride * _stride;
+                            memcpy(_reference, _roi, sizeof(_reference));
+                        });
+						if (movingRatio < _referenceRatio) {
+							// update reference
+                            copy(frame, _reference);
+						}
+
+                        ESP_LOGD(
+                            "RoI AnomalyDetection", 
+                            "roi: (x=%d, y=%d, width=%d, height=%d). moving points: %.2f%%", 
+                            coords.x,
+                            coords.y,
+                            coords.width,
+                            coords.height,
+                            moving_ratio
+                        );
+                        if (triggered() && !rate_limiter)
+                            return exception.set(rate.getRetryInMessage()).soft();
+
+                        if (triggered())
+                            rate_limiter.touch();
+
+                        return exception.clear();
+                    }
+
+
+                protected:
+                    float _x;
+                    float _y;
+                    float _w;
+                    float _h;
+                    uint8_t *_roi;
+
+                    /**
+                     * Copy RoI of frame into buffer
+                     */
+                    template<typename Frame>
+                    void copy(Frame& frame, uint8_t *dest) {
+                        for (int i = coords.y1; i < coords.y2; i++)
+                            memcpy(
+                                dest + coords.width * (i - coords.y1) * sizeof(uint16_t), 
+                                frame.data + (frame.width * i + coords.x) * sizeof(uint16_t), 
+                                coords.width * sizeof(uint16_t)
+                            );
+                    }
+            };
+        }
+    }
+}
+
+namespace eloq {
+    namespace anomaly {
+        // create class alias
+        class RoI : public Eloquent::Esp32cam::Anomaly::RoI {};
+    }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/eloquent_esp32cam/camera/rgb_565.h b/src/eloquent_esp32cam/camera/rgb_565.h
index 68dd0e11..c11cee44 100644
--- a/src/eloquent_esp32cam/camera/rgb_565.h
+++ b/src/eloquent_esp32cam/camera/rgb_565.h
@@ -21,6 +21,7 @@ namespace Eloquent {
                     size_t length;
                     size_t width;
                     size_t height;
+					jpg_scale_t scaling;
 
                     /**
                      * 
@@ -30,7 +31,8 @@ namespace Eloquent {
                         camera(cam),
                         length(0),
                         width(0),
-                        height(0) {
+                        height(0),
+						scaling(JPG_SCALE_8X) {
                     }
 
                     /**
@@ -94,8 +96,8 @@ namespace Eloquent {
                             return exception.set("Can't convert empty frame to RGB565");
 
                         if (!width) {
-                            width = camera->resolution.getWidth() / 8;
-                            height = camera->resolution.getHeight() / 8;
+                            width = camera->resolution.getWidth() >> scaling;
+                            height = camera->resolution.getHeight() >> scaling;
                             length = width * height;
 
                             ESP_LOGI("Camera", "Allocating %d bytes for %dx%d RGB565 image", length * 2, width, height);
@@ -106,7 +108,7 @@ namespace Eloquent {
                             return exception.set("Cannot allocate memory");
 
                         camera->mutex.threadsafe([this]() {
-                            if (!jpg2rgb565(camera->frame->buf, camera->frame->len, (uint8_t*) data, JPG_SCALE_8X))
+                            if (!jpg2rgb565(camera->frame->buf, camera->frame->len, (uint8_t*) data, scaling))
                                 exception.set("Error converting frame from JPEG to RGB565");
                         });
 
diff --git a/src/eloquent_esp32cam/motion/detection.h b/src/eloquent_esp32cam/motion/detection.h
index a62f9398..f345c894 100644
--- a/src/eloquent_esp32cam/motion/detection.h
+++ b/src/eloquent_esp32cam/motion/detection.h
@@ -131,7 +131,6 @@ namespace Eloquent {
                             movingRatio = ((float) movingPoints) / camera.rgb565.length * _stride * _stride;
                             copy(camera.rgb565);
                         });
-
                         ESP_LOGD("MotionDetection", "moving points ratio: %.2f", movingRatio);
 
                         // rate limit
@@ -143,7 +142,6 @@ namespace Eloquent {
 
                         return exception.clear();
                     }
-                    
                     /**
                      * @brief Convert to JSON
                      */
diff --git a/src/eloquent_esp32cam/transform/crop.h b/src/eloquent_esp32cam/transform/crop.h
index b45fd9b5..d018a683 100644
--- a/src/eloquent_esp32cam/transform/crop.h
+++ b/src/eloquent_esp32cam/transform/crop.h
@@ -42,8 +42,8 @@ namespace Eloquent {
                         _src.height = height;
                         _src.x1 = 0;
                         _src.y1 = 0;
-                        _src.x2 = width;
-                        _src.y2 = height;
+                        _src.x2 = width - 1;
+                        _src.y2 = height - 1;
 
                         return *this;
                     }
@@ -64,8 +64,8 @@ namespace Eloquent {
                         _out.height = height;
                         _out.x1 = 0;
                         _out.y1 = 0;
-                        _out.x2 = width;
-                        _out.y2 = height;
+                        _out.x2 = width - 1;
+                        _out.y2 = height - 1;
 
                         return *this;
                     }
@@ -93,13 +93,13 @@ namespace Eloquent {
                      */
                     Crop& squash() {
                         _src.x1 = 0;
-                        _src.x2 = _src.width;
+                        _src.x2 = _src.width - 1;
                         _src.y1 = 0;
-                        _src.y2 = _src.height;
+                        _src.y2 = _src.height - 1;
                         _out.x1 = 0;
-                        _out.x2 = _out.width;
+                        _out.x2 = _out.width - 1;
                         _out.y1 = 0;
-                        _out.y2 = _out.height;
+                        _out.y2 = _out.height - 1;
 
                         return *this;
                     }
@@ -114,12 +114,14 @@ namespace Eloquent {
 
                             _src.x1 = dx;
                             _src.y1 = dy;
-                            _src.x2 = _src.width - dx;
-                            _src.y2 = _src.height - dy;
+                            _src.x2 = _src.width - dx - 1;
+                            _src.y2 = _src.height - dy - 1;
                             _out.x1 = 0;
                             _out.y1 = 0;
-                            _out.x2 = _out.width;
-                            _out.y2 = _out.height;
+                            _out.x2 = _out.width - 1;
+                            _out.y2 = _out.height - 1;
+
+                            return *this;
                         }
                         else if (_out.width > _src.width) {
                             uint16_t dx = (_out.width - _src.width) / 2;
@@ -127,17 +129,35 @@ namespace Eloquent {
 
                             _out.x1 = dx;
                             _out.y1 = dy;
-                            _out.x2 = _out.width - dx;
-                            _out.y2 = _out.height - dy;
+                            _out.x2 = _out.width - dx - 1;
+                            _out.y2 = _out.height - dy - 1;
                             _src.x1 = 0;
                             _src.y1 = 0;
-                            _src.x2 = _src.width;
-                            _src.y2 = _src.height;
+                            _src.x2 = _src.width - 1;
+                            _src.y2 = _src.height - 1;
                         }
                         
                         return *this;
                     }
 
+                    /**
+                     * Manually set crop area origin
+                     * @param x
+                     * @param y
+                     * @return
+                     */
+                    Crop& offset(int16_t x, int16_t y) {
+                        if (x < 0) x += _src.width;
+                        if (y < 0) y += _src.height;
+
+                        _src.x1 = x;
+                        _src.x2 = x + _out.width - 1;
+                        _src.y1 = y;
+                        _src.y2 = y + _out.height - 1;
+
+                        return *this;
+                    }
+
                     /**
                      * No interpolation
                      */