spifftek70 / Drone-Footprints

GNU Affero General Public License v3.0
23 stars 13 forks source link

Geo-Rectify without converting to RGB colorspace #96

Closed nlraley closed 4 months ago

nlraley commented 4 months ago

Is it possible to Geo-Rectify without converting to the RGB color space? I'd like to processing and geo-rectify band specific images, which it does just fine, but it's changing the color space in doing so. I have no problems making changes to the process, I just don't know where to make the necessary changes.

Thanks

spifftek70 commented 4 months ago

Which bands? What drone, camera?

nlraley commented 4 months ago

DJI Mavic 3's NIR, R, G, and RE bands. I am trying to geo-rectify each individually, but the gdalinfo for one of them after processing is producing this: Band 1 Block=2580x1 Type=Byte, ColorInterp=Red NoData Value=0 Band 2 Block=2580x1 Type=Byte, ColorInterp=Green NoData Value=0 Band 3 Block=2580x1 Type=Byte, ColorInterp=Blue NoData Value=0

When the original source was this:

Band 1 Block=2592x1 Type=UInt16, ColorInterp=Gray

The geo-rectifying appears to be pretty accurate, it's just changing the color space and converting from the UInt16 to Byte.

spifftek70 commented 4 months ago

Utils.raster_utils.py

nlraley commented 4 months ago

Thanks. The dtype is already changed prior to calling rectify_and_warp_to_geotiff. Working my way up to see where it's getting wiped out.

So far, it seems the cv2.imread is returning the dtype of 8bit.

nlraley commented 4 months ago

If I load with the: jpeg_img = cv2.imread(image_path, cv2.IMREAD_UNCHANGED) inside of set_raster_extents.py, it's keeping the correct dtype and ndim value. However, it will fail on the call to:

if jpeg_img.ndim == 2: # Single band image adjImg = cv2.cvtColor(img_undistorted, cv2.COLOR_BGR2GRAY)

With the following error:

cv2.error: OpenCV(4.5.0) /code/SuperBuild/src/opencv/modules/imgproc/src/color.simd_helpers.hpp:92: error: (-2:Unspecified error) in function 'cv::impl::{anonymous}::CvtHelper<VScn, VDcn, VDepth, sizePolicy>::CvtHelper(cv::InputArray, cv::OutputArray, int) [with VScn = cv::impl::{anonymous}::Set<3, 4>; VDcn = cv::impl::{anonymous}::Set<1>; VDepth = cv::impl::{anonymous}::Set<0, 2, 5>; cv::impl::{anonymous}::SizePolicy sizePolicy = cv::impl::::NONE; cv::InputArray = const cv::_InputArray&; cv::OutputArray = const cv::_OutputArray&]' Invalid number of channels in input image: 'VScn::contains(scn)' where 'scn' is 1 /workspaces/flight-processor/drone-footprints/create_geotiffs.py(57)set_raster_extents() -> adjImg = cv2.cvtColor(img_undistorted, cv2.COLOR_BGR2GRAY)

If I do not load it with the IMREAD_UNCHANGED flag, the ndim type is identifying the ndim as 3, and is treating it as a cv2.COLOR_BGR2RGB.

Any recommendations?

nlraley commented 4 months ago

I am thinking if I make the following changes in

I also copied the source dtype in the lens correction logic in that function, I haven't tested that yet.

Next, I added the type support for uint16 in the array2ds call, and I believe that should suffice? BGR2GRAY doesn't seem correct to use for a single band, b/c you are going from a Blue, Green, Red to Gray, you are going from a Grey to Grey, right? I'm just sifting through the code and trying to understand the workflows and repurcussions.

nlraley commented 4 months ago

Interesting, the RGB composite image is reporting ndim of 2, so that's not going to work either. Is there any way to check the band count?

nlraley commented 4 months ago

I think this ends up being what is needed:

def set_raster_extents(image_path, dst_utf8_path, coordinate_array):
    try:
        jpeg_img = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
        if jpeg_img is None:
            logger.warning(f"File not found: {image_path}")
            return
        fixed_polygon = Polygon(coordinate_array)
        if config.lense_correction is True:
            try:
                focal_length = config.drone_properties['FocalLength']
                distance = config.center_distance
                cam_maker = config.drone_properties['CameraMake']
                cam_model = config.drone_properties['SensorModel']
                aperture = config.drone_properties['MaxApertureValue']

                # Load camera and lens from lensfun database
                db = lensfunpy.Database()
                cam = db.find_cameras(cam_maker, cam_model, True)[0]
                lens = db.find_lenses(cam, cam_maker, cam_model, True)[0]

                height, width = jpeg_img.shape[:2]
                mod = lensfunpy.Modifier(lens, cam.crop_factor, width, height)

                # Determine rasterio data type based on cv2_array data type
                if jpeg_img.dtype == np.uint8:
                    pixel_format = np.uint8
                elif jpeg_img.dtype == np.int16:
                    pixel_format = np.int16
                elif jpeg_img.dtype == np.uint16:
                    pixel_format = np.uint16
                elif jpeg_img.dtype == np.int32:
                    pixel_format = np.int32
                elif jpeg_img.dtype == np.float32:
                    pixel_format = np.float32
                elif jpeg_img.dtype == np.float64:
                    pixel_format = np.float64
                else:
                    logger.opt(exception=True).warning(f"Unsupported data type: {str(jpeg_img.dtype)}")

                mod.initialize(focal_length, aperture, distance, pixel_format=pixel_format)

                # Apply geometry distortion correction and obtain distortion maps
                maps = mod.apply_geometry_distortion()
                map_x = maps[:, :, 0]
                map_y = maps[:, :, 1]

                img_undistorted = cv2.remap(jpeg_img, map_x, map_y, interpolation=cv2.INTER_LANCZOS4)
            except IndexError as e:
                config.update_lense(False)
                img_undistorted = np.array(jpeg_img)
                logger.info(f"Cannot correct lens distortion. Camera properties not found in database.")
                logger.exception(f"Index error: {e} for {image_path}")
        else:
            img_undistorted = np.array(jpeg_img)

        if jpeg_img.ndim == 2:  # Single band image
            adjImg = img_undistorted
        if jpeg_img.ndim == 3:  # Multiband image
            adjImg = cv2.cvtColor(img_undistorted, cv2.COLOR_BGR2RGB)
        else:
            adjImg = cv2.cvtColor(img_undistorted, cv2.COLOR_BGR2RGBA)

        rectify_and_warp_to_geotiff(adjImg, dst_utf8_path, fixed_polygon, coordinate_array)
    except FileNotFoundError as e:
        logger.exception(f"File not found: {image_path}. {e}")
    except Exception as e:
        logger.exception(f"Error opening or processing image: {e}")

Then the dtype for the UINT16.

spifftek70 commented 4 months ago

I'm working it now. Stand up.

spifftek70 commented 4 months ago

Did you test that code?

nlraley commented 4 months ago

It's almost all working on my end. Like I said, I had to add that condition for the:

elif cv2_array.dtype == np.uint16:
        dtype = rasterio.uint16

in the array2ds call too. It's processing through and geo-rectifying them. The only piece I am missing now is that single band images have the UInt16 type, but the saved off file does have 4 bands still.

nlraley commented 4 months ago

Sorry, that condition should be an if, elif, else. I missed the else if when I copy and pasted here.

nlraley commented 4 months ago

I added:

            else:  # For grayscale images
                dst.write(cv2_array, 1)
                color_interpretations = [ColorInterp.gray]
                dst.colorinterp = color_interpretations[:bands]

on the else condition at line 187 in raster_utils.py to set the color interpretation to gray for the single band and I think that was the last piece needed. It now correctly reports:

Band 1 Block=2580x1 Type=UInt16, ColorInterp=Gray NoData Value=0

spifftek70 commented 4 months ago

Okay, adding that to the original function fixed it?

        else:  # For grayscale images
            dst.write(cv2_array, 1)
            color_interpretations = [ColorInterp.gray]
            dst.colorinterp = color_interpretations[:bands]
spifftek70 commented 4 months ago

For clarity, please provide the entire revised set_raster_extents() function

spifftek70 commented 4 months ago

Nevermind. I figured it out.

spifftek70 commented 4 months ago

Fixed

nlraley commented 4 months ago

Sorry, I was out for dinner. looks like you had found it.

For reference, here's what I had:

def set_raster_extents(image_path, dst_utf8_path, coordinate_array):
    try:
        jpeg_img = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
        if jpeg_img is None:
            logger.warning(f"File not found: {image_path}")
            return
        fixed_polygon = Polygon(coordinate_array)
        if config.lense_correction is True:
            try:
                focal_length = config.drone_properties['FocalLength']
                distance = config.center_distance
                cam_maker = config.drone_properties['CameraMake']
                cam_model = config.drone_properties['SensorModel']
                aperture = config.drone_properties['MaxApertureValue']

                # Load camera and lens from lensfun database
                db = lensfunpy.Database()
                cam = db.find_cameras(cam_maker, cam_model, True)[0]
                lens = db.find_lenses(cam, cam_maker, cam_model, True)[0]

                height, width = jpeg_img.shape[:2]
                mod = lensfunpy.Modifier(lens, cam.crop_factor, width, height)

                # Determine rasterio data type based on cv2_array data type
                if jpeg_img.dtype == np.uint8:
                    pixel_format = np.uint8
                elif jpeg_img.dtype == np.int16:
                    pixel_format = np.int16
                elif jpeg_img.dtype == np.uint16:
                    pixel_format = np.uint16
                elif jpeg_img.dtype == np.int32:
                    pixel_format = np.int32
                elif jpeg_img.dtype == np.float32:
                    pixel_format = np.float32
                elif jpeg_img.dtype == np.float64:
                    pixel_format = np.float64
                else:
                    logger.opt(exception=True).warning(f"Unsupported data type: {str(jpeg_img.dtype)}")

                mod.initialize(focal_length, aperture, distance, pixel_format=pixel_format)

                # Apply geometry distortion correction and obtain distortion maps
                maps = mod.apply_geometry_distortion()
                map_x = maps[:, :, 0]
                map_y = maps[:, :, 1]

                img_undistorted = cv2.remap(jpeg_img, map_x, map_y, interpolation=cv2.INTER_LANCZOS4)
            except IndexError as e:
                config.update_lense(False)
                img_undistorted = np.array(jpeg_img)
                logger.info(f"Cannot correct lens distortion. Camera properties not found in database.")
                logger.exception(f"Index error: {e} for {image_path}")
        else:
            img_undistorted = np.array(jpeg_img)

        if jpeg_img.ndim == 2:  # Single band image
            adjImg = img_undistorted
        elif jpeg_img.ndim == 3:  # Multiband image
            adjImg = cv2.cvtColor(img_undistorted, cv2.COLOR_BGR2RGB)
        else:
            adjImg = cv2.cvtColor(img_undistorted, cv2.COLOR_BGR2RGBA)        

        rectify_and_warp_to_geotiff(adjImg, dst_utf8_path, fixed_polygon, coordinate_array)
    except FileNotFoundError as e:
        logger.exception(f"File not found: {image_path}. {e}")
    except Exception as e:
        logger.exception(f"Error opening or processing image: {e}")

and

def array2ds(cv2_array, polygon_wkt):
    """
    Converts an OpenCV image array to a rasterio dataset with geospatial data.

    Parameters:
    - cv2_array: The OpenCV image array to convert.
    - polygon_wkt: Well-Known Text (WKT) representation of the polygon for spatial reference.
    - epsg_code: EPSG code for the spatial reference system (default: 4326 for WGS84).

    Returns:
    - rasterio dataset object with the image and geospatial data.
    """
    # Check input parameters
    if not isinstance(cv2_array, np.ndarray):
        logger.opt(exception=True).warning(f"cv2_array must be a numpy array.")
    if not isinstance(polygon_wkt, str):
        logger.opt(exception=True).warning(f"polygon_wkt must be a string.")
    if not isinstance(config.epsg_code, int):
        logger.opt(exception=True).warning(f"epsg_code must be an integer.")

    polygon = loads(polygon_wkt)
    minx, miny, maxx, maxy = polygon.bounds

    # Image dimensions and bands
    if len(cv2_array.shape) == 3:  # For color images
        height, width, bands = cv2_array.shape
    else:  # For grayscale images
        height, width = cv2_array.shape
        bands = 1

    # Determine rasterio data type based on cv2_array data type
    if cv2_array.dtype == np.uint8:
        dtype = rasterio.uint8
    elif cv2_array.dtype == np.int16:
        dtype = rasterio.int16
    elif cv2_array.dtype == np.uint16:
        dtype = rasterio.uint16
    elif cv2_array.dtype == np.int32:
        dtype = rasterio.int32
    elif cv2_array.dtype == np.float32:
        dtype = rasterio.float32
    elif cv2_array.dtype == np.float64:
        dtype = rasterio.float64
    else:
        logger.opt(exception=True).warning(f"Unsupported data type: {str(cv2_array.dtype)}")

    # Create and configure the rasterio dataset
    transform = from_bounds(minx, miny, maxx, maxy, width, height)
    crs = rasterio.crs.CRS.from_epsg(config.epsg_code)

    with rasterio.MemoryFile() as memfile:
        with memfile.open(driver='GTiff', height=height, width=width, count=bands, dtype=dtype, crs=crs,
                          transform=transform) as dst:
            if len(cv2_array.shape) == 3:  # For color images
                for i in range(1, bands + 1):
                    dst.write(cv2_array[:, :, i - 1], i)
                    # Set color interpretation for each band if applicable
                    if bands == 3:
                        color_interpretations = [ColorInterp.red, ColorInterp.green, ColorInterp.blue]
                        dst.colorinterp = color_interpretations[:bands]
                    elif bands == 4:
                        color_interpretations = [ColorInterp.red, ColorInterp.green, ColorInterp.blue,
                                                 ColorInterp.alpha]
                        dst.colorinterp = color_interpretations[:bands]
            else:  # For grayscale images
                dst.write(cv2_array, 1)
                color_interpretations = [ColorInterp.gray]
                dst.colorinterp = color_interpretations[:bands]
        return memfile.open()

Looks like you got it.

nlraley commented 4 months ago

So I’m comparing the geo-rectified single image, vs the ortho of multiple images from the same field via ODM, and while they are close, there’s a slight difference in rotation in the single image.

@.***

Is this to be expected based on the corrections that are performed while stitching the ortho within ODM based on the multiple images and their overlap, or would we expect them to be more similarly matched? The ODM ortho lines up pretty well with the satellite imagery, but there’s a slight tilt in the orientation on the single one. I didn’t want to create another issue if the difference is expected, but if it isn’t, I can provide the orthomosaic and the single shot in the field so it can be tested against if that would be beneficial.

I just didn’t know if I was missing any parameters or something that could make them more accurate. I ran the following command: python3 Drone_Footprints.py -i /workspaces/flight-processor/imagery/dfea0bcd-6ff6-4d22-81a9-b153f6be6417/workspace/ -o /workspaces/flight-processor/imagery/dfea0bcd-6ff6-4d22-81a9-b153f6be6417/workspace/working -m -d -l

I was under the assumption the camera band properties were being resolved via the drone_sensors.csv file, so I didn’t supply those.

Thanks,

Nathan

From: Dean @.> Sent: Friday, April 19, 2024 6:37 PM To: spifftek70/Drone-Footprints @.> Cc: Raley, Nathan @.>; Author @.> Subject: [External] Re: [spifftek70/Drone-Footprints] Geo-Rectify without converting to RGB colorspace (Issue #96)

Closed #96 as completed. — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread. Message ID: spifftek70/Drone-Footprints/issue/96/issue_event/12545793467@ github. com

Closed #96https://urldefense.com/v3/__https:/github.com/spifftek70/Drone-Footprints/issues/96__;!!CsmrWXz9mOkSc4Hdn1fjj00!2rF3UCT7sVvIGZ5z74q2Sgn1V33JBjt3niOx1oTgpbXQwLaaB1EezGym20fg5fToFoFtDmFjU_HV4Bvhw8TRwMQ0J9UD_OC9Ig$ as completed.

— Reply to this email directly, view it on GitHubhttps://urldefense.com/v3/__https:/github.com/spifftek70/Drone-Footprints/issues/96*event-12545793467__;Iw!!CsmrWXz9mOkSc4Hdn1fjj00!2rF3UCT7sVvIGZ5z74q2Sgn1V33JBjt3niOx1oTgpbXQwLaaB1EezGym20fg5fToFoFtDmFjU_HV4Bvhw8TRwMQ0J9URak9Jzw$, or unsubscribehttps://urldefense.com/v3/__https:/github.com/notifications/unsubscribe-auth/AETVHCIVRVRXO27FI2YNT7TY6GS77AVCNFSM6AAAAABGPPNMVSVHI2DSMVQWIX3LMV45UABCJFZXG5LFIV3GK3TUJZXXI2LGNFRWC5DJN5XDWMJSGU2DKNZZGM2DMNY__;!!CsmrWXz9mOkSc4Hdn1fjj00!2rF3UCT7sVvIGZ5z74q2Sgn1V33JBjt3niOx1oTgpbXQwLaaB1EezGym20fg5fToFoFtDmFjU_HV4Bvhw8TRwMQ0J9VjLrAOIQ$. You are receiving this because you authored the thread.Message ID: @.**@.>>

This email transmission, including any attachments, is intended solely for the addressee named above, and may contain confidential or privileged information. If you are not the intended recipient, be aware that any disclosure, copying, distribution or use of the contents of this e-mail is prohibited. If you have received this e-mail in error, please notify the sender immediately by reply email and destroy the message and its attachments.

spifftek70 commented 4 months ago

You testing Mavic 3E with Multispectral? Using RTK or no? If it is RTK, don't use -d as RTK has already been corrected for magnetic declination. Test with and without -d to check. Let me know, please.

nlraley commented 4 months ago

So without the -d, I get a rotation on the other side of the image:

Without the -d: @.***

With -d:

@.***

I think without the -d on the RGB image from the drone is better, but it’s still off on one side of the image. @.***https://www.greatamericaninsurancegroup.com/ Nathan Raley Senior Application Analyst and Developer Crop Division

132 S. Water St., Suite 500, Decatur, IL 62523 217.358.7313 mobile • 217.451.7857 office Connect with us! [Linkedin]https://linkedin.com/company/great-american-insurance [Facebook]https://www.facebook.com/GreatAmericanInsuranceGroup [GAIG.com]https://www.greatamericaninsurancegroup.com/

From: Dean @.> Sent: Friday, April 19, 2024 7:18 PM To: spifftek70/Drone-Footprints @.> Cc: Raley, Nathan @.>; Author @.> Subject: [External] Re: [spifftek70/Drone-Footprints] Geo-Rectify without converting to RGB colorspace (Issue #96)

You testing Mavic 3E with Multispectral? Using RTK or no? If it is RTK, don't use -d as RTK has already been corrected for magnetic declination. Test with and without -d to check. Let me know, please. — Reply to this email directly, view

You testing Mavic 3E with Multispectral? Using RTK or no? If it is RTK, don't use -d as RTK has already been corrected for magnetic declination. Test with and without -d to check. Let me know, please.

— Reply to this email directly, view it on GitHubhttps://urldefense.com/v3/__https:/github.com/spifftek70/Drone-Footprints/issues/96*issuecomment-2067416921__;Iw!!CsmrWXz9mOkSc4Hdn1fjj00!0cxW2E0YHwjSiygwxZqJBGG8GkVJ55sKF2brH66i5xPifEUmxWqvVI6qdCLpsZBHwYeIbi1U34eBItylYFjrbuqACv5TnfUAyw$, or unsubscribehttps://urldefense.com/v3/__https:/github.com/notifications/unsubscribe-auth/AETVHCLV47XPJVF7ZRDWTRLY6GX4JAVCNFSM6AAAAABGPPNMVSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANRXGQYTMOJSGE__;!!CsmrWXz9mOkSc4Hdn1fjj00!0cxW2E0YHwjSiygwxZqJBGG8GkVJ55sKF2brH66i5xPifEUmxWqvVI6qdCLpsZBHwYeIbi1U34eBItylYFjrbuqACv6tamRUJQ$. You are receiving this because you authored the thread.Message ID: @.**@.>>

This email transmission, including any attachments, is intended solely for the addressee named above, and may contain confidential or privileged information. If you are not the intended recipient, be aware that any disclosure, copying, distribution or use of the contents of this e-mail is prohibited. If you have received this e-mail in error, please notify the sender immediately by reply email and destroy the message and its attachments.

spifftek70 commented 4 months ago

When was the last time you calibrated your IMU and compass?

https://github.com/spifftek70/Drone-Footprints?tab=readme-ov-file#warning-tips-for-the-most-accurate-results

Here's a sample I have from M3M images

Screenshot 2024-04-20 at 5 06 49 AM
nlraley commented 4 months ago

It wasn’t from my flight, but one of our pilots. They are “supposed” to do it prior to each flight, and this was one I was in the field with him and I’m pretty sure he went through the steps prior to the flight. Like I mentioned, the ortho of the full image set positions them fine, there’s just a slight rotation in the single using the drone footprints library.

I’m looking into the feasability of ortho rectifying single images for some measurements and whatnot.

Get Outlook for iOShttps://aka.ms/o0ukef


From: Dean @.> Sent: Saturday, April 20, 2024 7:06:30 AM To: spifftek70/Drone-Footprints @.> Cc: Raley, Nathan @.>; Author @.> Subject: [External] Re: [spifftek70/Drone-Footprints] Geo-Rectify without converting to RGB colorspace (Issue #96)

When was the last time you calibrated your IMU and compass? https: //github. com/spifftek70/Drone-Footprints?tab=readme-ov-file#warning-tips-for-the-most-accurate-results — Reply to this email directly, view it on GitHub, or unsubscribe. You

When was the last time you calibrated your IMU and compass?

https://github.com/spifftek70/Drone-Footprints?tab=readme-ov-file#warning-tips-for-the-most-accurate-resultshttps://urldefense.com/v3/__https://github.com/spifftek70/Drone-Footprints?tab=readme-ov-file*warning-tips-for-the-most-accurate-results__;Iw!!CsmrWXz9mOkSc4Hdn1fjj00!0MUTlWTq0PJAoeB-f728ueCzXqG0XJKatwMihb2rWpTb-Sg3kq275OXBeTHqp0m0nbyPlvraaDTFnIYzsyO0riNk8Xd4gBdIjA$

— Reply to this email directly, view it on GitHubhttps://urldefense.com/v3/__https://github.com/spifftek70/Drone-Footprints/issues/96*issuecomment-2067653290__;Iw!!CsmrWXz9mOkSc4Hdn1fjj00!0MUTlWTq0PJAoeB-f728ueCzXqG0XJKatwMihb2rWpTb-Sg3kq275OXBeTHqp0m0nbyPlvraaDTFnIYzsyO0riNk8Xc5ar_Wdw$, or unsubscribehttps://urldefense.com/v3/__https://github.com/notifications/unsubscribe-auth/AETVHCNLNUW7OXEJZEXES6TY6JK4NAVCNFSM6AAAAABGPPNMVSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANRXGY2TGMRZGA__;!!CsmrWXz9mOkSc4Hdn1fjj00!0MUTlWTq0PJAoeB-f728ueCzXqG0XJKatwMihb2rWpTb-Sg3kq275OXBeTHqp0m0nbyPlvraaDTFnIYzsyO0riNk8Xcv_Q7PvA$. You are receiving this because you authored the thread.Message ID: @.***>

This email transmission, including any attachments, is intended solely for the addressee named above, and may contain confidential or privileged information. If you are not the intended recipient, be aware that any disclosure, copying, distribution or use of the contents of this e-mail is prohibited. If you have received this e-mail in error, please notify the sender immediately by reply email and destroy the message and its attachments.

spifftek70 commented 4 months ago

What is your email?

nlraley commented 4 months ago

@.***

Get Outlook for iOShttps://aka.ms/o0ukef


From: Dean @.> Sent: Saturday, April 20, 2024 11:42:05 AM To: spifftek70/Drone-Footprints @.> Cc: Raley, Nathan @.>; Author @.> Subject: [External] Re: [spifftek70/Drone-Footprints] Geo-Rectify without converting to RGB colorspace (Issue #96)

What is your email? — Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread. Message ID: spifftek70/Drone-Footprints/issues/96/2067725458@ github. com ‍ ‍ ‍ ‍ ‍ ‍ ‍ ‍ ‍ ‍ ‍

What is your email?

— Reply to this email directly, view it on GitHubhttps://urldefense.com/v3/__https://github.com/spifftek70/Drone-Footprints/issues/96*issuecomment-2067725458__;Iw!!CsmrWXz9mOkSc4Hdn1fjj00!xFm8POHRccRNoyjUFlzEyI1lSt5YTDkjdaGGlbS4UzZM5QGEPbD4ebBYIa0YzFzU7ZQbSwAm-p4z2Lv0vusm_qd8-6gTJTWX-g$, or unsubscribehttps://urldefense.com/v3/__https://github.com/notifications/unsubscribe-auth/AETVHCN6EJ7JO7EZTU7LVXTY6KLF3AVCNFSM6AAAAABGPPNMVSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANRXG4ZDKNBVHA__;!!CsmrWXz9mOkSc4Hdn1fjj00!xFm8POHRccRNoyjUFlzEyI1lSt5YTDkjdaGGlbS4UzZM5QGEPbD4ebBYIa0YzFzU7ZQbSwAm-p4z2Lv0vusm_qd8-6jmkOFPDw$. You are receiving this because you authored the thread.Message ID: @.***>

This email transmission, including any attachments, is intended solely for the addressee named above, and may contain confidential or privileged information. If you are not the intended recipient, be aware that any disclosure, copying, distribution or use of the contents of this e-mail is prohibited. If you have received this e-mail in error, please notify the sender immediately by reply email and destroy the message and its attachments.

spifftek70 commented 4 months ago

that is a link to the App Store.
Guessed an email to your work based on your username here and company link. sent test