From 8683021fcad6db4a3a216fcfe492d5a08dc0b997 Mon Sep 17 00:00:00 2001 From: kshitijrajsharma Date: Wed, 13 Mar 2024 15:32:58 +0545 Subject: [PATCH 1/9] Add multimasks optiion in tasks --- backend/core/tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/core/tasks.py b/backend/core/tasks.py index a3ec613b..7730660d 100644 --- a/backend/core/tasks.py +++ b/backend/core/tasks.py @@ -188,6 +188,7 @@ def train_model( rasterize=True, rasterize_options=["binary"], georeference_images=True, + multimasks=True, ) # train From fa84c60bce5033c4493ed90adea24fad41f67daa Mon Sep 17 00:00:00 2001 From: kshtiijrajsharma Date: Sun, 24 Mar 2024 21:19:56 +0545 Subject: [PATCH 2/9] feat(multimasks) : Added multimasks input parameter to API and frontend --- backend/core/tasks.py | 54 ++++--- backend/core/views.py | 58 +++++--- .../AIModels/AIModelEditor/AIModelEditor.js | 136 +++++++++++++++++- 3 files changed, 215 insertions(+), 33 deletions(-) diff --git a/backend/core/tasks.py b/backend/core/tasks.py index 7730660d..6bc7d81e 100644 --- a/backend/core/tasks.py +++ b/backend/core/tasks.py @@ -3,23 +3,14 @@ import os import shutil import sys +import tarfile import traceback from shutil import rmtree -import tarfile import hot_fair_utilities import ramp.utils import tensorflow as tf from celery import shared_task -from django.conf import settings -from django.contrib.gis.db.models.aggregates import Extent -from django.contrib.gis.geos import GEOSGeometry -from django.shortcuts import get_object_or_404 -from django.utils import timezone -from hot_fair_utilities import preprocess, train -from hot_fair_utilities.training import run_feedback -from predictor import download_imagery, get_start_end_download_coords - from core.models import AOI, Feedback, FeedbackAOI, FeedbackLabel, Label, Training from core.serializers import ( AOISerializer, @@ -29,6 +20,14 @@ LabelFileSerializer, ) from core.utils import bbox, is_dir_empty +from django.conf import settings +from django.contrib.gis.db.models.aggregates import Extent +from django.contrib.gis.geos import GEOSGeometry +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from hot_fair_utilities import preprocess, train +from hot_fair_utilities.training import run_feedback +from predictor import download_imagery, get_start_end_download_coords logger = logging.getLogger(__name__) @@ -37,6 +36,7 @@ DEFAULT_TILE_SIZE = 256 + def xz_folder(folder_path, output_filename, remove_original=False): """ Compresses a folder and its contents into a .tar.xz file and optionally removes the original folder. @@ -47,8 +47,8 @@ def xz_folder(folder_path, output_filename, remove_original=False): - remove_original: If True, the original folder is removed after compression. """ - if not output_filename.endswith('.tar.xz'): - output_filename += '.tar.xz' + if not output_filename.endswith(".tar.xz"): + output_filename += ".tar.xz" with tarfile.open(output_filename, "w:xz") as tar: tar.add(folder_path, arcname=os.path.basename(folder_path)) @@ -67,6 +67,9 @@ def train_model( source_imagery, feedback=None, freeze_layers=False, + multimasks=False, + input_contact_spacing=0.75, + input_boundary_width=0.5, ): training_instance = get_object_or_404(Training, id=training_id) training_instance.status = "RUNNING" @@ -182,13 +185,22 @@ def train_model( # preprocess model_input_image_path = f"{base_path}/input" preprocess_output = f"/{base_path}/preprocessed" + + if multimasks: + logger.info( + "Using multiple masks for training : background, footprint, boundary, contact" + ) + else: + logger.info("Using binary masks for training : background, footprint") preprocess( input_path=model_input_image_path, output_path=preprocess_output, rasterize=True, rasterize_options=["binary"], georeference_images=True, - multimasks=True, + multimasks=multimasks, + input_contact_spacing=input_contact_spacing, + input_boundary_width=input_boundary_width, ) # train @@ -273,9 +285,19 @@ def train_model( f.write(json.dumps(aoi_serializer.data)) # copy aois and labels to preprocess output before compressing it to tar - shutil.copyfile(os.path.join(output_path, "aois.geojson"), os.path.join(preprocess_output,'aois.geojson')) - shutil.copyfile(os.path.join(output_path, "labels.geojson"), os.path.join(preprocess_output,'labels.geojson')) - xz_folder(preprocess_output, os.path.join(output_path, "preprocessed.tar.xz"), remove_original=True) + shutil.copyfile( + os.path.join(output_path, "aois.geojson"), + os.path.join(preprocess_output, "aois.geojson"), + ) + shutil.copyfile( + os.path.join(output_path, "labels.geojson"), + os.path.join(preprocess_output, "labels.geojson"), + ) + xz_folder( + preprocess_output, + os.path.join(output_path, "preprocessed.tar.xz"), + remove_original=True, + ) # now remove the ramp-data all our outputs are copied to our training workspace shutil.rmtree(base_path) diff --git a/backend/core/views.py b/backend/core/views.py index 03f4ffe4..affbb89b 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -81,6 +81,15 @@ class DatasetViewSet( class TrainingSerializer( serializers.ModelSerializer ): # serializers are used to translate models objects to api + + multimasks = serializers.BooleanField(required=False, default=False) + input_contact_spacing = serializers.FloatField( + required=False, default=0.75, min_value=0, max_value=5 + ) + input_boundary_width = serializers.FloatField( + required=False, default=0.5, min_value=0, max_value=5 + ) + class Meta: model = Training fields = "__all__" # defining all the fields to be included in curd for now , we can restrict few if we want @@ -128,6 +137,10 @@ def create(self, validated_data): # create the model instance instance = Training.objects.create(**validated_data) + multimasks = validated_data.get("multimasks", False) + input_contact_spacing = validated_data.get("input_contact_spacing", 0.75) + input_boundary_width = validated_data.get("input_boundary_width", 0.5) + # run your function here task = train_model.delay( dataset_id=instance.model.dataset.id, @@ -138,9 +151,14 @@ def create(self, validated_data): source_imagery=instance.source_imagery or instance.model.dataset.source_imagery, freeze_layers=instance.freeze_layers, + multimasks=multimasks, + input_contact_spacing=input_contact_spacing, + input_boundary_width=input_boundary_width, ) if not instance.source_imagery: instance.source_imagery = instance.model.dataset.source_imagery + if multimasks: + instance.description += f" Multimask params (ct/bw): {input_contact_spacing}/{input_boundary_width}" instance.task_id = task.id instance.save() print(f"Saved train model request to queue with id {task.id}") @@ -192,7 +210,7 @@ class FeedbackLabelViewset(viewsets.ModelViewSet): bbox_filter_field = "geom" filter_backends = ( InBBoxFilter, # it will take bbox like this api/v1/label/?in_bbox=-90,29,-89,35 , - DjangoFilterBackend + DjangoFilterBackend, ) bbox_filter_include_overlapping = True filterset_fields = ["feedback_aoi", "feedback_aoi__training"] @@ -343,9 +361,9 @@ def download_training_data(request, dataset_id: int): response = HttpResponse(open(zip_temp_path, "rb")) response.headers["Content-Type"] = "application/x-zip-compressed" - response.headers[ - "Content-Disposition" - ] = f"attachment; filename=training_{dataset_id}_all_data.zip" + response.headers["Content-Disposition"] = ( + f"attachment; filename=training_{dataset_id}_all_data.zip" + ) return response else: # "error": "File Doesn't Exist or has been cleared up from system", @@ -553,12 +571,16 @@ def post(self, request, *args, **kwargs): zoom_level=zoom_level, tms_url=source, tile_size=DEFAULT_TILE_SIZE, - confidence=deserialized_data["confidence"] / 100 - if "confidence" in deserialized_data - else 0.5, - tile_overlap_distance=deserialized_data["tile_overlap_distance"] - if "tile_overlap_distance" in deserialized_data - else 0.15, + confidence=( + deserialized_data["confidence"] / 100 + if "confidence" in deserialized_data + else 0.5 + ), + tile_overlap_distance=( + deserialized_data["tile_overlap_distance"] + if "tile_overlap_distance" in deserialized_data + else 0.15 + ), ) print( f"It took {round(time.time()-start_time)}sec for generating predictions" @@ -569,12 +591,16 @@ def post(self, request, *args, **kwargs): if use_josm_q is True: feature["geometry"] = othogonalize_poly( feature["geometry"], - maxAngleChange=deserialized_data["max_angle_change"] - if "max_angle_change" in deserialized_data - else 15, - skewTolerance=deserialized_data["skew_tolerance"] - if "skew_tolerance" in deserialized_data - else 15, + maxAngleChange=( + deserialized_data["max_angle_change"] + if "max_angle_change" in deserialized_data + else 15 + ), + skewTolerance=( + deserialized_data["skew_tolerance"] + if "skew_tolerance" in deserialized_data + else 15 + ), ) print( diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js b/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js index 66538b8a..c32a232f 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js @@ -13,6 +13,9 @@ import axios from "../../../../axios"; import { useMutation, useQuery } from "react-query"; import Popup from "./Popup"; +import { Accordion, AccordionSummary, AccordionDetails } from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; + import OSMUser from "../../../Shared/OSMUser"; import SaveIcon from "@material-ui/icons/Save"; import { Checkbox, FormControlLabel } from "@mui/material"; @@ -35,6 +38,10 @@ const AIModelEditor = (props) => { const [sourceImagery, setSourceImagery] = React.useState(null); const [freezeLayers, setFreezeLayers] = useState(false); + const [multimasks, setMultimasks] = React.useState(false); + const [inputContactSpacing, setInputContactSpacing] = React.useState(0.75); + const [inputBoundaryWidth, setInputBoundaryWidth] = React.useState(0.5); + const [popupRowData, setPopupRowData] = useState(null); const [feedbackCount, setFeedbackCount] = useState(0); const [feedbackData, setFeedbackData] = useState(null); @@ -123,6 +130,9 @@ const AIModelEditor = (props) => { model: id, zoom_level: zoomLevel, description: description, + input_contact_spacing: inputContactSpacing, + input_boundary_width: inputBoundaryWidth, + multimasks: multimasks, }; const headers = { "access-token": accessToken, @@ -310,7 +320,7 @@ const AIModelEditor = (props) => { helperText={ A short description to document why you submitted this - training + training or extra additional info } type="text" @@ -339,6 +349,130 @@ const AIModelEditor = (props) => { */} + + + } + aria-controls="panel1a-content" + id="panel1a-header" + sx={{ + minHeight: "32px", + height: "32px", + background: "white !important", + "& .MuiAccordionSummary-content": { + margin: "0", + alignItems: "center", + }, + "& .MuiAccordionSummary-expandIconWrapper": { + padding: "0", + "&.Mui-expanded": { + transform: "rotate(180deg)", + }, + }, + // Prevent changes in background or elevation when expanded + "&.Mui-expanded": { + minHeight: "32px", + margin: "0", + }, + "&:hover": { + background: "white", + }, + "&.Mui-focusVisible": { + backgroundColor: "white", + }, + }} + > + + Advanced Parameters + + + + + + setMultimasks(e.target.checked)} + name="multimasks" + /> + } + label={ + + Take boundary of footprints into account during + training + + } + sx={{ margin: "0" }} + /> + + + + Enter the distance in meters to extend the area around + each building. This will be used to find points where + buildings come into contact or are in close proximity + to one another. For example, entering '0.75' will + explore areas within 75 centimers outside the original + building shapes to detect nearby buildings + + } + value={inputContactSpacing} + fullWidth + onChange={(e) => setInputContactSpacing(e.target.value)} + InputProps={{ + sx: { fontSize: "0.875rem", height: "40px" }, + }} + InputLabelProps={{ + sx: { fontSize: "0.875rem" }, + }} + /> + + + + Specify the width in meters to reduce the original + building shape inwardly, creating a boundary or margin + around each building. A smaller value creates a + tighter boundary close to the building's edges, while + a larger value creates a wider surrounding area. For + example, entering '0.5' will create a boundary that is + 50 centimeters inside from the original building + edges. + + } + fullWidth + onChange={(e) => setInputBoundaryWidth(e.target.value)} + InputProps={{ + sx: { fontSize: "0.875rem", height: "40px" }, + }} + InputLabelProps={{ + sx: { fontSize: "0.875rem" }, + }} + /> + + + + + From 7911a3d4259e9ff87bf407c70f86b69ff35c089d Mon Sep 17 00:00:00 2001 From: kshtiijrajsharma Date: Sun, 24 Mar 2024 21:32:21 +0545 Subject: [PATCH 3/9] pop unnecessary values for instance creation in training --- backend/core/views.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/core/views.py b/backend/core/views.py index affbb89b..befb8e10 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -135,12 +135,18 @@ def create(self, validated_data): user = self.context["request"].user validated_data["created_by"] = user # create the model instance - instance = Training.objects.create(**validated_data) - multimasks = validated_data.get("multimasks", False) input_contact_spacing = validated_data.get("input_contact_spacing", 0.75) input_boundary_width = validated_data.get("input_boundary_width", 0.5) + pop_keys = ["multimasks", "input_contact_spacing", "input_boundary_width"] + + for key in pop_keys: + if key in validated_data.keys(): + validated_data.pop(key) + + instance = Training.objects.create(**validated_data) + # run your function here task = train_model.delay( dataset_id=instance.model.dataset.id, From c1b1ca03b330589fd85e6306609a7cafd6f905f8 Mon Sep 17 00:00:00 2001 From: kshtiijrajsharma Date: Sun, 24 Mar 2024 22:28:21 +0545 Subject: [PATCH 4/9] Change default value for contact spacing --- .../components/Layout/AIModels/AIModelEditor/AIModelEditor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js b/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js index c32a232f..c6dc4c18 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js @@ -39,8 +39,8 @@ const AIModelEditor = (props) => { const [freezeLayers, setFreezeLayers] = useState(false); const [multimasks, setMultimasks] = React.useState(false); - const [inputContactSpacing, setInputContactSpacing] = React.useState(0.75); - const [inputBoundaryWidth, setInputBoundaryWidth] = React.useState(0.5); + const [inputContactSpacing, setInputContactSpacing] = React.useState(1); + const [inputBoundaryWidth, setInputBoundaryWidth] = React.useState(0.25); const [popupRowData, setPopupRowData] = useState(null); const [feedbackCount, setFeedbackCount] = useState(0); From 14883081077f01b8d68631fefd0db05a717865e5 Mon Sep 17 00:00:00 2001 From: kshtiijrajsharma Date: Wed, 27 Mar 2024 17:55:38 +0545 Subject: [PATCH 5/9] (bug): multimask_params : fix inconsistency in different zoom levels , added pixel as input instead of meters --- backend/core/tasks.py | 4 ++-- backend/core/views.py | 8 ++++---- .../AIModels/AIModelEditor/AIModelEditor.js | 19 +++++++++---------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/backend/core/tasks.py b/backend/core/tasks.py index 6bc7d81e..f8726a4e 100644 --- a/backend/core/tasks.py +++ b/backend/core/tasks.py @@ -68,8 +68,8 @@ def train_model( feedback=None, freeze_layers=False, multimasks=False, - input_contact_spacing=0.75, - input_boundary_width=0.5, + input_contact_spacing=8, + input_boundary_width=3, ): training_instance = get_object_or_404(Training, id=training_id) training_instance.status = "RUNNING" diff --git a/backend/core/views.py b/backend/core/views.py index befb8e10..8eb54f48 100644 --- a/backend/core/views.py +++ b/backend/core/views.py @@ -83,11 +83,11 @@ class TrainingSerializer( ): # serializers are used to translate models objects to api multimasks = serializers.BooleanField(required=False, default=False) - input_contact_spacing = serializers.FloatField( - required=False, default=0.75, min_value=0, max_value=5 + input_contact_spacing = serializers.IntegerField( + required=False, default=8, min_value=0, max_value=20 ) - input_boundary_width = serializers.FloatField( - required=False, default=0.5, min_value=0, max_value=5 + input_boundary_width = serializers.IntegerField( + required=False, default=3, min_value=0, max_value=10 ) class Meta: diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js b/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js index c6dc4c18..e8aaabd0 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js @@ -39,8 +39,8 @@ const AIModelEditor = (props) => { const [freezeLayers, setFreezeLayers] = useState(false); const [multimasks, setMultimasks] = React.useState(false); - const [inputContactSpacing, setInputContactSpacing] = React.useState(1); - const [inputBoundaryWidth, setInputBoundaryWidth] = React.useState(0.25); + const [inputContactSpacing, setInputContactSpacing] = React.useState(8); + const [inputBoundaryWidth, setInputBoundaryWidth] = React.useState(3); const [popupRowData, setPopupRowData] = useState(null); const [feedbackCount, setFeedbackCount] = useState(0); @@ -421,12 +421,12 @@ const AIModelEditor = (props) => { type="number" helperText={ - Enter the distance in meters to extend the area around + Enter the distance in pixels to extend the area around each building. This will be used to find points where buildings come into contact or are in close proximity - to one another. For example, entering '0.75' will - explore areas within 75 centimers outside the original - building shapes to detect nearby buildings + to one another. For example, entering '8' will explore + areas within 8 pixels outside the original building + shapes to detect nearby buildings } value={inputContactSpacing} @@ -449,14 +449,13 @@ const AIModelEditor = (props) => { value={inputBoundaryWidth} helperText={ - Specify the width in meters to reduce the original + Specify the width in pixels to reduce the original building shape inwardly, creating a boundary or margin around each building. A smaller value creates a tighter boundary close to the building's edges, while a larger value creates a wider surrounding area. For - example, entering '0.5' will create a boundary that is - 50 centimeters inside from the original building - edges. + example, entering '3' will create a boundary that 3 + pixles inside from the original building edges. } fullWidth From aa0e5d2605072feba98d63c2c624cc981ba9a17a Mon Sep 17 00:00:00 2001 From: kshitijrajsharma Date: Wed, 3 Apr 2024 18:08:15 +0545 Subject: [PATCH 6/9] Add debug mode --- backend/aiproject/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/aiproject/settings.py b/backend/aiproject/settings.py index 2159d5d0..2d62f438 100644 --- a/backend/aiproject/settings.py +++ b/backend/aiproject/settings.py @@ -11,7 +11,7 @@ """ import os - +import logging import dj_database_url import environ from corsheaders.defaults import default_headers @@ -184,6 +184,7 @@ STATIC_ROOT = os.path.join(BASE_DIR, "api_static") if DEBUG: + logging.info("Enabling oauthlib insecure transport in debug mode") os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" From 01714e76d6f080e434136d309991a96916eeb232 Mon Sep 17 00:00:00 2001 From: kshtiijrajsharma Date: Wed, 1 May 2024 13:31:07 +0545 Subject: [PATCH 7/9] Add multimasks in tasks --- backend/core/tasks.py | 1 + backend/fAIr-utilities | 1 + 2 files changed, 2 insertions(+) create mode 160000 backend/fAIr-utilities diff --git a/backend/core/tasks.py b/backend/core/tasks.py index f8726a4e..7dcd32d5 100644 --- a/backend/core/tasks.py +++ b/backend/core/tasks.py @@ -231,6 +231,7 @@ def train_model( model="ramp", model_home=os.environ["RAMP_HOME"], freeze_layers=freeze_layers, + multimasks=multimasks ) # copy final model to output diff --git a/backend/fAIr-utilities b/backend/fAIr-utilities new file mode 160000 index 00000000..93debb40 --- /dev/null +++ b/backend/fAIr-utilities @@ -0,0 +1 @@ +Subproject commit 93debb40704c94fb4f89ca93f660e8b5c0fdb747 From c44170ec094c57c79423f245334ccffe667bf8a3 Mon Sep 17 00:00:00 2001 From: kshitijrajsharma Date: Wed, 8 May 2024 17:46:09 +0545 Subject: [PATCH 8/9] add multimasks to feedback as arguments --- backend/core/tasks.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/backend/core/tasks.py b/backend/core/tasks.py index 7dcd32d5..4fb9245f 100644 --- a/backend/core/tasks.py +++ b/backend/core/tasks.py @@ -11,6 +11,15 @@ import ramp.utils import tensorflow as tf from celery import shared_task +from django.conf import settings +from django.contrib.gis.db.models.aggregates import Extent +from django.contrib.gis.geos import GEOSGeometry +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from hot_fair_utilities import preprocess, train +from hot_fair_utilities.training import run_feedback +from predictor import download_imagery, get_start_end_download_coords + from core.models import AOI, Feedback, FeedbackAOI, FeedbackLabel, Label, Training from core.serializers import ( AOISerializer, @@ -20,14 +29,6 @@ LabelFileSerializer, ) from core.utils import bbox, is_dir_empty -from django.conf import settings -from django.contrib.gis.db.models.aggregates import Extent -from django.contrib.gis.geos import GEOSGeometry -from django.shortcuts import get_object_or_404 -from django.utils import timezone -from hot_fair_utilities import preprocess, train -from hot_fair_utilities.training import run_feedback -from predictor import download_imagery, get_start_end_download_coords logger = logging.getLogger(__name__) @@ -219,6 +220,7 @@ def train_model( ), model_home=os.environ["RAMP_HOME"], epoch_size=epochs, + multimasks=multimasks, batch_size=batch_size, freeze_layers=freeze_layers, ) @@ -231,7 +233,7 @@ def train_model( model="ramp", model_home=os.environ["RAMP_HOME"], freeze_layers=freeze_layers, - multimasks=multimasks + multimasks=multimasks, ) # copy final model to output From e0b73538dfb2e98a5e98cf9c3e667917e95e6442 Mon Sep 17 00:00:00 2001 From: kshitijrajsharma Date: Thu, 1 Aug 2024 07:45:55 +0545 Subject: [PATCH 9/9] Update multimask default parameters --- .../components/Layout/AIModels/AIModelEditor/AIModelEditor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js b/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js index e8aaabd0..a069796f 100644 --- a/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js +++ b/frontend/src/components/Layout/AIModels/AIModelEditor/AIModelEditor.js @@ -39,8 +39,8 @@ const AIModelEditor = (props) => { const [freezeLayers, setFreezeLayers] = useState(false); const [multimasks, setMultimasks] = React.useState(false); - const [inputContactSpacing, setInputContactSpacing] = React.useState(8); - const [inputBoundaryWidth, setInputBoundaryWidth] = React.useState(3); + const [inputContactSpacing, setInputContactSpacing] = React.useState(4); + const [inputBoundaryWidth, setInputBoundaryWidth] = React.useState(2); const [popupRowData, setPopupRowData] = useState(null); const [feedbackCount, setFeedbackCount] = useState(0);