diff --git a/changedetectionio/__init__.py b/changedetectionio/__init__.py index 5340ad9e..93c47426 100644 --- a/changedetectionio/__init__.py +++ b/changedetectionio/__init__.py @@ -782,6 +782,48 @@ def changedetection_app(config=None, datastore_o=None): return redirect(url_for('index')) + + @app.route("/diff/image/", methods=['GET']) + @login_required + def diff_image_history_page(uuid): + + # More for testing, possible to return the first/only + if uuid == 'first': + uuid = list(datastore.data['watching'].keys()).pop() + + extra_stylesheets = [url_for('static_content', group='styles', filename='diff.css')] + try: + watch = datastore.data['watching'][uuid] + except KeyError: + flash("No history found for the specified link, bad link?", "error") + return redirect(url_for('index')) + + history = watch.history + dates = list(history.keys()) + + if len(dates) < 2: + flash("Not enough saved change detection snapshots to produce a report.", "error") + return redirect(url_for('index')) + + previous_version = dates[-2] + + output = render_template("diff-image.html", + watch=watch, + extra_stylesheets=extra_stylesheets, + versions=dates[:-1], # All except current/last + uuid=uuid, + newest_version_timestamp=dates[-1], + current_previous_version=str(previous_version), + current_diff_url=watch['url'], + extra_title=" - Diff - {}".format(watch['title'] if watch['title'] else watch['url']), + left_sticky=True, + last_error=watch['last_error'], + last_error_text=watch.get_error_text(), + last_error_screenshot=watch.get_error_snapshot() + ) + return output + + @app.route("/diff/", methods=['GET']) @login_required def diff_history_page(uuid): @@ -947,6 +989,61 @@ def changedetection_app(config=None, datastore_o=None): return output + @app.route("/preview/image//") + def render_single_image(uuid, history_timestamp): + + watch = datastore.data['watching'].get(uuid) + dates = list(watch.history.keys()) + + + if not history_timestamp or history_timestamp == 'None': + history_timestamp = dates[-2] + + + filename = watch.history[history_timestamp] + with open(filename, 'rb') as f: + img = f.read() + + response = make_response(img) + + response.headers['Content-type'] = 'image/png' + response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' + response.headers['Pragma'] = 'no-cache' + response.headers['Expires'] = 0 + + return response + + + + # Diff renderer for images + # Renders the diff which includes the red box around what changes + # We always compare the newest against whatever compare_date we are given + @app.route("/diff/image//") + def render_diff_image(uuid, compare_date): + from changedetectionio import image_diff + + from flask import make_response + watch = datastore.data['watching'].get(uuid) + + dates = list(watch.history.keys()) + if len(dates) < 2: + flash("Not enough saved change detection snapshots to produce a report.", "error") + return redirect(url_for('index')) + + if not compare_date or compare_date == 'None': + compare_date = dates[-2] + + new_img = watch.history[watch.newest_history_key] + prev_img = watch.history[compare_date] + + img = image_diff.render_diff(new_img, prev_img) + + resp = make_response(img) + resp.headers['Content-Type'] = 'image/jpeg' + return resp + + + @app.route("/settings/notification-logs", methods=['GET']) @login_required def notification_logs(): @@ -1095,12 +1192,16 @@ def changedetection_app(config=None, datastore_o=None): return redirect(url_for('index')) url = request.form.get('url').strip() + fetch_processor =request.form.get('fetch_processor').strip() if datastore.url_exists(url): flash('The URL {} already exists'.format(url), "error") return redirect(url_for('index')) add_paused = request.form.get('edit_and_watch_submit_button') != None - new_uuid = datastore.add_watch(url=url, tag=request.form.get('tag').strip(), extras={'paused': add_paused}) + new_uuid = datastore.add_watch(url=url, + tag=request.form.get('tag').strip(), + extras={'paused': add_paused, 'fetch_processor': fetch_processor} + ) if not add_paused and new_uuid: @@ -1241,7 +1342,7 @@ def changedetection_app(config=None, datastore_o=None): return redirect(url_for('index')) - @app.route("/api/share-url", methods=['GET']) + @app.route("/api/r-url", methods=['GET']) @login_required def form_share_put_watch(): """Given a watch UUID, upload the info and return a share-link diff --git a/changedetectionio/image_diff.py b/changedetectionio/image_diff.py new file mode 100644 index 00000000..1f033327 --- /dev/null +++ b/changedetectionio/image_diff.py @@ -0,0 +1,40 @@ +from skimage.metrics import structural_similarity as compare_ssim +import argparse +import imutils +import cv2 + +# From https://www.pyimagesearch.com/2017/06/19/image-difference-with-opencv-and-python/ +def render_diff(fpath_imageA, fpath_imageB): + + imageA = cv2.imread(fpath_imageA) + imageB = cv2.imread(fpath_imageB) + + # convert the images to grayscale + grayA = cv2.cvtColor(imageA, cv2.COLOR_BGR2GRAY) + grayB = cv2.cvtColor(imageB, cv2.COLOR_BGR2GRAY) + + # compute the Structural Similarity Index (SSIM) between the two + # images, ensuring that the difference image is returned + (score, diff) = compare_ssim(grayA, grayB, full=True) + diff = (diff * 255).astype("uint8") + print("SSIM: {}".format(score)) + + # threshold the difference image, followed by finding contours to + # obtain the regions of the two input images that differ + thresh = cv2.threshold(diff, 0, 255, + cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] + cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, + cv2.CHAIN_APPROX_SIMPLE) + cnts = imutils.grab_contours(cnts) + + # loop over the contours + for c in cnts: + # compute the bounding box of the contour and then draw the + # bounding box on both input images to represent where the two + # images differ + (x, y, w, h) = cv2.boundingRect(c) + cv2.rectangle(imageA, (x, y), (x + w, y + h), (0, 0, 255), 2) + cv2.rectangle(imageB, (x, y), (x + w, y + h), (0, 0, 255), 2) + + #return cv2.imencode('.jpg', imageB)[1].tobytes() + return cv2.imencode('.jpg', imageA)[1].tobytes() diff --git a/changedetectionio/model/Watch.py b/changedetectionio/model/Watch.py index bb227920..86eb4a09 100644 --- a/changedetectionio/model/Watch.py +++ b/changedetectionio/model/Watch.py @@ -27,7 +27,7 @@ class model(dict): 'extract_text': [], # Extract text by regex after filters 'extract_title_as_title': False, 'fetch_backend': None, - 'fetch_processor': None, # default None, json_html_plaintext, image + 'fetch_processor': 'json_html_plaintext', # json_html_plaintext, image 'filter_failure_notification_send': strtobool(os.getenv('FILTER_FAILURE_NOTIFICATION_SEND_DEFAULT', 'True')), 'headers': {}, # Extra headers to send 'ignore_text': [], # List of text to ignore when calculating the comparison checksum diff --git a/changedetectionio/templates/diff-image.html b/changedetectionio/templates/diff-image.html new file mode 100644 index 00000000..f8077822 --- /dev/null +++ b/changedetectionio/templates/diff-image.html @@ -0,0 +1,51 @@ +{% extends 'base.html' %} + +{% block content %} + +
+

Differences

+
+
+ {% if versions|length >= 1 %} + + + + {% endif %} +
+
+ +
+ +
+ + +
+ + + + + + +{% endblock %} \ No newline at end of file diff --git a/changedetectionio/templates/edit.html b/changedetectionio/templates/edit.html index d3bbf221..64f0c5ff 100644 --- a/changedetectionio/templates/edit.html +++ b/changedetectionio/templates/edit.html @@ -31,7 +31,6 @@ {% if 'text-filters-and-triggers' in enabled_tabs %}
  • Filters & Triggers
  • {%endif%} -
  • Notifications
  • diff --git a/changedetectionio/templates/preview-image.html b/changedetectionio/templates/preview-image.html new file mode 100644 index 00000000..3834489d --- /dev/null +++ b/changedetectionio/templates/preview-image.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% block content %} +
    +

    Preview

    +
    + +
    + +
    + +{% endblock %} \ No newline at end of file diff --git a/changedetectionio/templates/watch-overview.html b/changedetectionio/templates/watch-overview.html index ae5ad43e..670cd1a3 100644 --- a/changedetectionio/templates/watch-overview.html +++ b/changedetectionio/templates/watch-overview.html @@ -116,7 +116,12 @@ class="recheck pure-button button-small pure-button-primary">{% if watch.uuid in queued_uuids %}Queued{% else %}Recheck{% endif %} Edit {% if watch.history_n >= 2 %} - Diff + {% if watch.fetch_processor == "image" %} + Diff + {% else %} + Diff + {% endif %} + {% else %} {% if watch.history_n == 1 or (watch.history_n ==0 and watch.error_text_ctime )%} Preview diff --git a/requirements.txt b/requirements.txt index 55b0947e..44d0cdc9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,4 +46,7 @@ werkzeug ~= 2.0.0 imagehash ~= 4.3.0 pillow +scikit-image +imutils +opencv-python python-magic