From 9f771ecfa7f9a5414abeef8717d1d25981b861e3 Mon Sep 17 00:00:00 2001 From: jordancaraballo Date: Fri, 18 Oct 2024 11:59:18 +0000 Subject: [PATCH 1/2] Adding major changes to saving user seconds per point #38, counter fixes #28, and lat lon options --- eo_validation/async_write.py | 16 ++++ eo_validation/validation_dashboard.py | 111 +++++++++++++++++++++----- 2 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 eo_validation/async_write.py diff --git a/eo_validation/async_write.py b/eo_validation/async_write.py new file mode 100644 index 0000000..8655996 --- /dev/null +++ b/eo_validation/async_write.py @@ -0,0 +1,16 @@ +import threading + + +# Inheriting the base class 'Thread' +class AsyncWriteGDF(threading.Thread): + + def __init__(self): + + # calling superclass init + threading.Thread.__init__(self) + + def save(self, gdf_object, output_filename): + + gdf_object.to_file( + output_filename, layer='validation', driver="GPKG") + return diff --git a/eo_validation/validation_dashboard.py b/eo_validation/validation_dashboard.py index d82a443..8b274fe 100644 --- a/eo_validation/validation_dashboard.py +++ b/eo_validation/validation_dashboard.py @@ -2,10 +2,12 @@ import pwd import copy import math +import time import socket import ipysheet import ipyleaflet import numpy as np +import pandas as pd import geopandas as gpd import rioxarray as rxr import ipywidgets as widgets @@ -37,6 +39,7 @@ Popup ) from shapely.geometry import shape +from eo_validation.async_write import AsyncWriteGDF if os.getenv("JUPYTERHUB_SERVICE_PREFIX") is not None: @@ -260,6 +263,12 @@ def __init__(self, **kwargs): self._markers_dict = dict() self._marker_counter = -1 + self._current_marker_id = None + self._current_time = None + self._seconds_per_point = None + + self.async_writer = AsyncWriteGDF() + # Adding default Google Basemap google_satellite_basemap = TileLayer( url='https://mt0.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', @@ -484,6 +493,8 @@ def add_markers( validation_points['burnt'] = 0 validation_points['confidence'] = 1 validation_points['verified'] = 'false' + validation_points['date'] = None + validation_points['seconds_taken'] = None # Create ipysheet object self._validation_sheet = ipysheet.sheet( @@ -616,9 +627,11 @@ def add_polygon_markers( # Extract output filename if None available and doing offline points if self.output_filename is None: + # self.output_filename = os.path.join( + # self.output_dir, f"{Path(in_filename).stem}.gpkg") self.output_filename = os.path.join( self.output_dir, f"{Path(in_filename).stem}.gpkg") - + # Case #1: student is already working on the points if os.path.isfile(self.output_filename): validation_points = self.load_gpkg(self.output_filename) @@ -634,6 +647,8 @@ def add_polygon_markers( validation_points['burnt'] = 0 validation_points['confidence'] = 1 validation_points['verified'] = False # 'false' + validation_points['date'] = None + validation_points['seconds_taken'] = None # Create ipysheet object self._validation_sheet = ipysheet.sheet( @@ -715,6 +730,27 @@ def create_property_widgets(self, properties): ) point_id_widget._property_key = 'ID' + + # print("PRE UPDATE MARKER", self._current_marker_id) + self._current_marker_id = property_items['ID'] + # print("POST UPDATE MARKER", self._current_marker_id) + + # (y, x) as (lat, lon) + + point_coords_widget_y = widgets.Text( + value=str(property_items['y']), + description='Lat:', + disabled=True + ) + point_coords_widget_y._property_key = 'y' + + point_coords_widget_x = widgets.Text( + value=str(property_items['x']), + description='Lon:', + disabled=True + ) + point_coords_widget_x._property_key = 'x' + checked_widget = widgets.Checkbox( value=verified_option, description='Verified:', @@ -722,8 +758,26 @@ def create_property_widgets(self, properties): ) checked_widget._property_key = 'verified' + def changed_checked_widget(b): + + self._seconds_per_point = round( + time.time() - self._current_time, 4) + self._feature['properties']['date'] = str(pd.Timestamp.now()) + self._feature['properties']['seconds_taken'] = self._seconds_per_point + self._feature['properties']['verified'] = True + + # updating the information with new data + self.geo_data_layer.geo_dataframe.loc[ + self.geo_data_layer.geo_dataframe['ID'] + == self._feature['properties']['ID'], + self._feature['properties'].keys()] = self._feature['properties'].values() + + checked_widget.observe(changed_checked_widget) + popup = [ point_id_widget, + point_coords_widget_y, + point_coords_widget_x, radio_check_widget, radio_burn_widget, radio_confidence_widget, @@ -733,12 +787,22 @@ def create_property_widgets(self, properties): return popup def on_click_polygon_object(self, event, feature, **kwargs): + + # get current time + self._current_time = time.time() + self._feature = feature + + self._feature['properties'] = self.geo_data_layer.geo_dataframe.loc[ + self.geo_data_layer.geo_dataframe['ID'] + == self._feature['properties']['ID']].to_dict(orient='records')[0] + # Dynamically create input widgets for each property - property_widgets = self.create_property_widgets(feature['properties']) + self.property_widgets = self.create_property_widgets( + self._feature['properties']) save_button = widgets.Button(description="Save") - geom_type = feature['geometry']['type'] + geom_type = self._feature['geometry']['type'] centroid = self.calculate_centroid( - feature['geometry']['coordinates'], geom_type) + self._feature['geometry']['coordinates'], geom_type) box_layout = widgets.Layout( display='flex', @@ -748,30 +812,31 @@ def on_click_polygon_object(self, event, feature, **kwargs): # Create and open the popup popup_content = widgets.VBox( - property_widgets + [save_button], layout=box_layout) + self.property_widgets + [save_button], layout=box_layout) - popup = Popup( + self._popup = Popup( location=centroid, child=popup_content, close_button=True, auto_close=False, - close_on_escape_key=True, - min_width=320 + close_on_escape_key=False, + min_width=320, + name='Observations' ) - self.add_layer(popup) + self.add_layer(self._popup) def save_changes(_): original_data = copy.deepcopy(self.geo_data_layer.data) - original_feature = copy.deepcopy(feature) + original_feature = copy.deepcopy(self._feature) # Update the properties with the new values - for widget in property_widgets: - feature['properties'][widget._property_key] = widget.value + for widget in self.property_widgets: + self._feature['properties'][widget._property_key] = widget.value for i, f in enumerate(original_data['features']): if f == original_feature: - original_data['features'][i] = feature + original_data['features'][i] = self._feature break # Update the GeoJSON layer to reflect the changes @@ -781,20 +846,21 @@ def save_changes(_): # updating the information with new data self.geo_data_layer.geo_dataframe.loc[ self.geo_data_layer.geo_dataframe['ID'] - == feature['properties']['ID'], - feature['properties'].keys()] = feature['properties'].values() + == self._feature['properties']['ID'], + self._feature['properties'].keys()] = self._feature['properties'].values() # saving output self.geo_data_layer.geo_dataframe.to_file( self.output_filename, layer='validation', driver="GPKG") # Close the popup by removing it from the map - self.remove_layer(popup) + self.remove_layer(self._popup) self.center = tuple( list(self._markers_dict)[self._marker_counter]) self.zoom = self.default_zoom + # verified_widget.observe(save_changes) save_button.on_click(save_changes) def save_gpkg(self, df, output_filename, layer="validation"): @@ -822,13 +888,20 @@ def load_gpkg(self, input_filename): if 'verified' not in gdf.columns: gdf['verified'] = False - # get the points that have been verified if any - self._marker_counter = gdf[ - 'verified'][gdf['verified']].last_valid_index() + if self.filter_points_by is not None: + verified_list = gdf[gdf['Group'] == self.filter_points_by][ + 'verified'].tolist() + else: + verified_list = gdf['verified'].tolist() + + self._marker_counter = [ + i for i, x in enumerate(verified_list) if not x][0] - 1 if self._marker_counter is None: self._marker_counter = -1 + print("MARKER COUNTER", self._marker_counter) + return gdf def _main_toolbar(self): From ed87c8567136f6f42cef30d902f78fc0232c787d Mon Sep 17 00:00:00 2001 From: jordancaraballo Date: Fri, 18 Oct 2024 12:01:28 +0000 Subject: [PATCH 2/2] Adding the 'Other' class to the list --- .../bin_peng/ValidationDashboard-Peng.ipynb | 49 ++++++------------- 1 file changed, 16 insertions(+), 33 deletions(-) diff --git a/notebooks/examples/bin_peng/ValidationDashboard-Peng.ipynb b/notebooks/examples/bin_peng/ValidationDashboard-Peng.ipynb index aecfd4a..441e788 100644 --- a/notebooks/examples/bin_peng/ValidationDashboard-Peng.ipynb +++ b/notebooks/examples/bin_peng/ValidationDashboard-Peng.ipynb @@ -34,21 +34,10 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "3020b230-dcf3-486a-af7e-4568a7160c10", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Downloading WhiteboxTools pre-compiled binary for first time use ...\n", - "Decompressing WhiteboxTools_linux_amd64.zip ...\n", - "WhiteboxTools package directory: /srv/conda/envs/notebook/lib/python3.9/site-packages/whitebox\n", - "Unexpected error: \n" - ] - } - ], + "outputs": [], "source": [ "import os\n", "import sys\n", @@ -66,25 +55,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "10f1da5e-8255-4bfe-918f-52fe1881ee87", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b29cc7284f984091be779db51466a7b8", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "ValidationDashboard(center=[4, -70], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_titleā€¦" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "dashboard = ValidationDashboard(\n", " default_max_zoom=80,\n", @@ -93,14 +67,15 @@ " center=[4, -70],\n", " marker_type='polygon',\n", " validation_classes=[\n", - " 'Evergreen Forest',\n", + " 'Cropland (herbaceous)',\n", " 'Deciduous Forest',\n", + " 'Evergreen Forest',\n", " 'Grassland',\n", " 'Shrubland',\n", - " 'Cropland (herbaceous)',\n", " 'Urban',\n", " 'Wetland',\n", - " 'Water'\n", + " 'Water',\n", + " 'Other'\n", " ],\n", " filter_points_by='Group1',\n", " output_dir='/home/jovyan/efs/BinPeng_Colombia/validation',\n", @@ -108,6 +83,14 @@ ")\n", "display(dashboard)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7c227d0-9cac-4772-957b-b89442829111", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": {