SAVI vs NDVI vs EVI: Which Vegetation Index Should Agricultural Scientists Use?

Introduction: The Index Problem Nobody Talks About

In Part 1 of this series, I showed you how to calculate NDVI with Python. NDVI is the most widely used vegetation index in the world — and for good reason. It works well in most situations.

But NDVI has a problem. Actually, two problems.

Problem 1: In areas with sparse vegetation — early crop season, semi-arid regions, or degraded lands — NDVI gets confused by soil brightness. A bright sandy soil can give NDVI values similar to a moderately healthy crop. Your crop health map becomes unreliable.

Problem 2: In areas with very dense vegetation — mature paddy at peak season, dense forest cover — NDVI saturates. Values pile up near 0.8–0.9 regardless of actual differences in canopy health. You lose the ability to distinguish between a good crop and an excellent crop.

Two researchers developed solutions:

  • SAVI (Soil Adjusted Vegetation Index) — solves Problem 1
  • EVI (Enhanced Vegetation Index) — solves both problems

In this post I will explain all three indices, show you exactly when to use each one, and give you complete Python code to calculate all three from Sentinel-2 data.

By the end you will know:

  • The mathematical difference between NDVI, SAVI, and EVI
  • Which index to use for which agricultural situation in India
  • How to calculate all three in Python with real Sentinel-2 data
  • How to compare them visually side by side
  • The practical decision framework for choosing your index

Quick Reference: The Three Indices at a Glance

Before going deep — here is the summary table for those who want the answer fast:

IndexFormulaBest ForAvoid When
NDVI(NIR – Red) / (NIR + Red)General crop monitoring, established canopySparse vegetation, very dense canopy
SAVI(NIR – Red) / (NIR + Red + L) × (1 + L)Sparse vegetation, early season, dryland farming, semi-arid areasDense canopy (use EVI instead)
EVIG × (NIR – Red) / (NIR + C1×Red – C2×Blue + L)Dense canopy, areas with atmospheric haze, high biomassSimple situations (NDVI is easier)

Understanding NDVI — And Its Limits

We covered NDVI in detail in Part 1. To summarise:

NDVI = (NIR - Red) / (NIR + Red)

What makes NDVI work: Healthy vegetation absorbs red light for photosynthesis and reflects near-infrared strongly. This contrast gives NDVI its signal.

What breaks NDVI:

Limitation 1 — Soil Brightness Effect

In early crop season or sparse canopy situations, a significant portion of what the satellite sees is bare soil between plants — not the plants themselves. Bright soils (sandy soils, dry soils) reflect both red and NIR, pushing NDVI values up artificially. Dark soils do the opposite.

This is a real problem in Indian dryland agriculture — Rajasthan, Maharashtra’s Vidarbha, parts of Karnataka — where soils are bright and canopy cover during early season is low.

Result: Your NDVI map shows moderately healthy vegetation when the ground truth is mostly soil with scattered seedlings.

Limitation 2 — Saturation at High Biomass

At leaf area index (LAI) above approximately 3, NDVI stops responding to further increases in vegetation density. Values cluster between 0.80 and 0.90 regardless of whether you are looking at average paddy or exceptional paddy.

Result: You cannot distinguish high-performing fields from very high-performing fields during peak season.


SAVI — Soil Adjusted Vegetation Index

SAVI was developed by Huete (1988) specifically to address the soil brightness problem.

The Formula

SAVI = ((NIR - Red) / (NIR + Red + L)) × (1 + L)

Where L is the soil brightness correction factor:

  • L = 1.0 — very low vegetation cover (bare soil dominant)
  • L = 0.5 — intermediate vegetation cover (most common, recommended default)
  • L = 0.25 — high vegetation cover (dense canopy)
  • L = 0.0 — SAVI becomes identical to NDVI

The L factor dampens the soil contribution to the spectral signal. When canopy cover is low, L = 0.5 gives significantly more accurate vegetation estimates than NDVI.

When to Use SAVI in Indian Agriculture

Agricultural SituationUse SAVI?Reason
Early Kharif season (June–July)✅ YesSparse canopy, soil dominant
Dryland farming (Rajasthan, Vidarbha)✅ YesBright soils, low canopy density
Rabi wheat early stage (Nov–Dec)✅ YesSparse seedling stage
Degraded pasture assessment✅ YesMixed soil/sparse grass
Semi-arid horticulture (mango orchards, early stage)✅ YesInter-row soil visible
Peak Kharif paddy (Aug–Sep)❌ NoDense canopy — use NDVI or EVI
Forest monitoring❌ NoHigh biomass — use EVI

SAVI in Python

import rasterio
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')

# ── Load bands ───────────────────────────────────────────────────────────
# Update paths to your Sentinel-2 files
red_path  = "T46RBN_20231005_B04_10m.tif"   # Band 4 — Red
nir_path  = "T46RBN_20231005_B08_10m.tif"   # Band 8 — NIR

with rasterio.open(red_path) as src:
    red     = src.read(1).astype(float)
    profile = src.profile

with rasterio.open(nir_path) as src:
    nir = src.read(1).astype(float)

# ── Calculate SAVI ───────────────────────────────────────────────────────
L = 0.5  # Standard soil brightness correction factor

# Avoid division by zero
denominator_savi = nir + red + L
denominator_savi[denominator_savi == 0] = np.nan

savi = ((nir - red) / denominator_savi) * (1 + L)

print("SAVI Statistics:")
print(f"  Min:  {np.nanmin(savi):.4f}")
print(f"  Max:  {np.nanmax(savi):.4f}")
print(f"  Mean: {np.nanmean(savi):.4f}")
print(f"  Std:  {np.nanstd(savi):.4f}")

SAVI value interpretation:

SAVI ValueVegetation Status
> 0.5Dense, healthy vegetation
0.3 – 0.5Moderate vegetation
0.1 – 0.3Sparse vegetation
0.0 – 0.1Very sparse / bare soil
< 0.0Non-vegetation (water, built-up)

Note: SAVI values are generally lower than NDVI for the same scene because the L factor reduces the overall magnitude. Do not compare SAVI and NDVI values directly — they are on different scales.


EVI — Enhanced Vegetation Index

EVI was developed by Huete et al. (2002) for NASA’s MODIS sensor. It addresses both NDVI limitations simultaneously — soil background AND atmospheric effects AND canopy saturation.

The Formula

EVI = G × (NIR - Red) / (NIR + C1 × Red - C2 × Blue + L)

Standard coefficients (MODIS/Sentinel-2):

  • G = 2.5 — gain factor
  • C1 = 6.0 — aerosol resistance coefficient (Red)
  • C2 = 7.5 — aerosol resistance coefficient (Blue)
  • L = 1.0 — canopy background adjustment

The Blue band term (C2 × Blue) is the key difference from NDVI and SAVI. It corrects for aerosol scattering in the atmosphere — important in India where haze, dust, and smoke can significantly affect satellite signals, especially in north India during winter months.

When to Use EVI in Indian Agriculture

Agricultural SituationUse EVI?Reason
Peak season dense paddy (Aug–Sep, Assam)✅ YesAvoids NDVI saturation
Dense sugarcane monitoring✅ YesVery high biomass crop
Forest and agroforestry assessment✅ YesHigh canopy, saturation issue
North India winter (haze season)✅ YesAtmospheric correction built in
Biomass estimation studies✅ YesLinear response at high LAI
Early season sparse crops❌ MarginalSAVI is simpler and equally good
Simple, quick crop health check❌ NoNDVI is faster and sufficient

EVI in Python

EVI requires the Blue band (Band 2 for Sentinel-2) in addition to Red and NIR:

# ── Load Blue band (needed for EVI) ─────────────────────────────────────
blue_path = "T46RBN_20231005_B02_10m.tif"   # Band 2 — Blue

with rasterio.open(blue_path) as src:
    blue = src.read(1).astype(float)

# ── EVI coefficients ─────────────────────────────────────────────────────
G  = 2.5   # Gain factor
C1 = 6.0   # Aerosol resistance — Red
C2 = 7.5   # Aerosol resistance — Blue
L  = 1.0   # Canopy background

# ── Calculate EVI ────────────────────────────────────────────────────────
# Sentinel-2 L2A reflectance values are scaled by 10000
# Divide by 10000 to get actual reflectance (0–1 range)
nir_r  = nir  / 10000.0
red_r  = red  / 10000.0
blue_r = blue / 10000.0

denominator_evi = nir_r + C1 * red_r - C2 * blue_r + L
denominator_evi[denominator_evi == 0] = np.nan

evi = G * (nir_r - red_r) / denominator_evi

# Clip to valid EVI range (-1 to 1)
evi = np.clip(evi, -1, 1)

print("EVI Statistics:")
print(f"  Min:  {np.nanmin(evi):.4f}")
print(f"  Max:  {np.nanmax(evi):.4f}")
print(f"  Mean: {np.nanmean(evi):.4f}")
print(f"  Std:  {np.nanstd(evi):.4f}")

Important note on reflectance scaling: Sentinel-2 L2A products store reflectance values multiplied by 10,000 (so 0.3 reflectance is stored as 3000). For EVI calculation you must divide by 10,000 first. For NDVI and SAVI this scaling cancels out in the ratio — but for EVI it does not, because of the additive L term.


Calculating All Three Together and Comparing

Here is the complete script that calculates NDVI, SAVI, and EVI together and produces a side-by-side comparison map:

"""
Vegetation Index Comparison — NDVI vs SAVI vs EVI
Author: Data Science with DEB | dibyendudeb.com
Part 3 of Agricultural Data Science with Python Series

Calculates and compares three vegetation indices
from Sentinel-2 data for agricultural monitoring.

Requirements:
    pip install rasterio numpy matplotlib
"""

import rasterio
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import warnings
warnings.filterwarnings('ignore')

# ─── CONFIGURATION ──────────────────────────────────────────────────────────
RED_PATH  = "T46RBN_20231005_B04_10m.tif"   # Band 4 — Red
NIR_PATH  = "T46RBN_20231005_B08_10m.tif"   # Band 8 — NIR
BLUE_PATH = "T46RBN_20231005_B02_10m.tif"   # Band 2 — Blue (EVI only)

STUDY_NAME = "Kamrup District, Assam — October 2023"

# SAVI soil correction factor
L_SAVI = 0.5

# EVI coefficients
G, C1, C2, L_EVI = 2.5, 6.0, 7.5, 1.0

# ─── LOAD BANDS ─────────────────────────────────────────────────────────────
print("Loading Sentinel-2 bands...")
with rasterio.open(RED_PATH) as src:
    red     = src.read(1).astype(float)
    profile = src.profile
with rasterio.open(NIR_PATH) as src:
    nir  = src.read(1).astype(float)
with rasterio.open(BLUE_PATH) as src:
    blue = src.read(1).astype(float)
print("✓ Bands loaded")

# ─── CALCULATE NDVI ─────────────────────────────────────────────────────────
denom_ndvi = nir + red
denom_ndvi[denom_ndvi == 0] = np.nan
ndvi = (nir - red) / denom_ndvi

# ─── CALCULATE SAVI ─────────────────────────────────────────────────────────
denom_savi = nir + red + L_SAVI
denom_savi[denom_savi == 0] = np.nan
savi = ((nir - red) / denom_savi) * (1 + L_SAVI)

# ─── CALCULATE EVI ──────────────────────────────────────────────────────────
# Scale to 0–1 reflectance
nir_r, red_r, blue_r = nir / 10000, red / 10000, blue / 10000

denom_evi = nir_r + C1 * red_r - C2 * blue_r + L_EVI
denom_evi[denom_evi == 0] = np.nan
evi = G * (nir_r - red_r) / denom_evi
evi = np.clip(evi, -1, 1)

# ─── PRINT STATISTICS ───────────────────────────────────────────────────────
print("\n" + "=" * 50)
print(f"VEGETATION INDEX COMPARISON — {STUDY_NAME}")
print("=" * 50)
print(f"{'Index':<8} {'Min':>8} {'Max':>8} {'Mean':>8} {'Std':>8}")
print("-" * 50)
for name, arr in [('NDVI', ndvi), ('SAVI', savi), ('EVI', evi)]:
    print(f"{name:<8} "
          f"{np.nanmin(arr):>8.4f} "
          f"{np.nanmax(arr):>8.4f} "
          f"{np.nanmean(arr):>8.4f} "
          f"{np.nanstd(arr):>8.4f}")
print("=" * 50)

# ─── VISUALISE — SIDE BY SIDE COMPARISON ────────────────────────────────────
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
fig.suptitle(f"Vegetation Index Comparison\n{STUDY_NAME}",
             fontsize=14, fontweight='bold')

index_config = [
    ('NDVI', ndvi, -0.2, 0.9,
     'Normalized Difference Vegetation Index'),
    ('SAVI', savi, -0.2, 0.7,
     'Soil Adjusted Vegetation Index (L=0.5)'),
    ('EVI',  evi,  -0.2, 0.8,
     'Enhanced Vegetation Index'),
]

for ax, (name, arr, vmin, vmax, subtitle) in zip(axes, index_config):
    im = ax.imshow(arr, cmap='RdYlGn', vmin=vmin, vmax=vmax)
    ax.set_title(f"{name}\n{subtitle}", fontsize=10, fontweight='bold')
    ax.axis('off')
    cbar = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
    cbar.set_label(f"{name} Value", rotation=270, labelpad=15)

plt.tight_layout()
plt.savefig("vegetation_index_comparison.png", dpi=200, bbox_inches='tight')
plt.show()
print("\n✓ Comparison map saved as vegetation_index_comparison.png")

# ─── SCATTER PLOT — NDVI vs SAVI vs EVI ─────────────────────────────────────
# Sample random pixels for scatter plot (full array is too large to plot)
np.random.seed(42)
mask = ~(np.isnan(ndvi) | np.isnan(savi) | np.isnan(evi))
flat_ndvi = ndvi[mask].flatten()
flat_savi = savi[mask].flatten()
flat_evi  = evi[mask].flatten()

# Sample 5000 points
n_sample = min(5000, len(flat_ndvi))
idx = np.random.choice(len(flat_ndvi), n_sample, replace=False)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
fig.suptitle("Index Relationships — How SAVI and EVI Differ from NDVI",
             fontsize=13, fontweight='bold')

# NDVI vs SAVI
ax1.scatter(flat_ndvi[idx], flat_savi[idx],
            alpha=0.3, s=5, c='#2ECC71', edgecolors='none')
ax1.plot([-0.3, 1.0], [-0.3, 1.0], 'r--', linewidth=1.5,
         label='1:1 line (if equal)')
ax1.set_xlabel('NDVI', fontsize=12)
ax1.set_ylabel('SAVI', fontsize=12)
ax1.set_title('NDVI vs SAVI\n(Points below 1:1 = SAVI corrects for soil)',
              fontsize=10)
ax1.legend(fontsize=9)
ax1.grid(True, alpha=0.3)
ax1.set_xlim(-0.3, 1.0)
ax1.set_ylim(-0.3, 0.8)

# NDVI vs EVI
ax2.scatter(flat_ndvi[idx], flat_evi[idx],
            alpha=0.3, s=5, c='#3498DB', edgecolors='none')
ax2.plot([-0.3, 1.0], [-0.3, 1.0], 'r--', linewidth=1.5,
         label='1:1 line (if equal)')
ax2.set_xlabel('NDVI', fontsize=12)
ax2.set_ylabel('EVI', fontsize=12)
ax2.set_title('NDVI vs EVI\n(EVI diverges at high values = less saturation)',
              fontsize=10)
ax2.legend(fontsize=9)
ax2.grid(True, alpha=0.3)
ax2.set_xlim(-0.3, 1.0)
ax2.set_ylim(-0.3, 0.9)

plt.tight_layout()
plt.savefig("ndvi_savi_evi_scatter.png", dpi=200, bbox_inches='tight')
plt.show()
print("✓ Scatter plot saved as ndvi_savi_evi_scatter.png")

# ─── SAVE ALL THREE AS GEOTIFFS ─────────────────────────────────────────────
profile.update({'dtype': rasterio.float32, 'count': 1, 'nodata': -9999})

for name, arr in [('ndvi', ndvi), ('savi', savi), ('evi', evi)]:
    output_path = f"{name}_output.tif"
    arr_save = arr.copy()
    arr_save[np.isnan(arr_save)] = -9999
    with rasterio.open(output_path, 'w', **profile) as dst:
        dst.write(arr_save.astype(rasterio.float32), 1)
    print(f"✓ {name.upper()} saved as {output_path}")

print("\n✓ All outputs complete.")

Real Comparison: What Changes Between Indices?

Let me show you with simulated values what actually changes when you switch indices. This is based on typical Sentinel-2 reflectance values for different agricultural land covers in India:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Typical Sentinel-2 reflectance values for Indian agricultural scenes
# (scaled 0-1 after dividing by 10000)
land_covers = {
    'Bare Soil (Bright)':       {'Blue': 0.18, 'Red': 0.22, 'NIR': 0.28},
    'Bare Soil (Dark)':         {'Blue': 0.05, 'Red': 0.07, 'NIR': 0.10},
    'Early Paddy (Jun-Jul)':    {'Blue': 0.04, 'Red': 0.06, 'NIR': 0.20},
    'Mid Paddy (Aug)':          {'Blue': 0.03, 'Red': 0.04, 'NIR': 0.38},
    'Peak Paddy (Sep)':         {'Blue': 0.02, 'Red': 0.03, 'NIR': 0.52},
    'Late Paddy (Oct)':         {'Blue': 0.03, 'Red': 0.06, 'NIR': 0.42},
    'Wheat Early (Nov-Dec)':    {'Blue': 0.05, 'Red': 0.08, 'NIR': 0.22},
    'Wheat Peak (Feb)':         {'Blue': 0.02, 'Red': 0.04, 'NIR': 0.50},
    'Sugarcane (Peak)':         {'Blue': 0.02, 'Red': 0.03, 'NIR': 0.58},
    'Water Body':               {'Blue': 0.08, 'Red': 0.05, 'NIR': 0.02},
}

results = []
for cover, bands in land_covers.items():
    B = bands['Blue']
    R = bands['Red']
    N = bands['NIR']

    # NDVI
    ndvi_val = (N - R) / (N + R) if (N + R) != 0 else np.nan

    # SAVI (L=0.5)
    L = 0.5
    savi_val = ((N - R) / (N + R + L)) * (1 + L) if (N + R + L) != 0 else np.nan

    # EVI
    G, C1, C2, L_e = 2.5, 6.0, 7.5, 1.0
    denom = N + C1 * R - C2 * B + L_e
    evi_val = G * (N - R) / denom if denom != 0 else np.nan

    results.append({
        'Land Cover': cover,
        'NDVI': round(ndvi_val, 3),
        'SAVI': round(savi_val, 3),
        'EVI':  round(evi_val, 3)
    })

df_results = pd.DataFrame(results)
print(df_results.to_string(index=False))

Sample output:

Land Cover                NDVI    SAVI     EVI
Bare Soil (Bright)       0.119   0.078   0.088
Bare Soil (Dark)         0.176   0.115   0.127
Early Paddy (Jun-Jul)    0.538   0.359   0.347
Mid Paddy (Aug)          0.810   0.541   0.575
Peak Paddy (Sep)         0.890   0.594   0.658
Late Paddy (Oct)         0.750   0.500   0.543
Wheat Early (Nov-Dec)    0.467   0.311   0.304
Wheat Peak (Feb)         0.852   0.568   0.617
Sugarcane (Peak)         0.902   0.601   0.679
Water Body              -0.429  -0.286  -0.199

Key observations from this table:

  1. Bare Soil (Bright) vs Bare Soil (Dark): NDVI gives 0.119 vs 0.176 — a difference of 0.057 purely from soil colour, not vegetation. SAVI reduces this to 0.078 vs 0.115 — a difference of 0.037. SAVI is more stable across soil types.
  2. Peak Paddy vs Sugarcane: NDVI gives 0.890 vs 0.902 — nearly indistinguishable despite real differences in biomass. EVI gives 0.658 vs 0.679 — still close but with more room to work with at the high end.
  3. Early Paddy: All three indices correctly show low values — but NDVI (0.538) is notably higher than SAVI (0.359) and EVI (0.347). The NDVI value is inflated by soil background. SAVI and EVI are more accurate here.

The Decision Framework — Which Index to Use

Use this flowchart logic every time you start a new analysis:

START
  │
  ├─ Is your vegetation sparse or early season?
  │   (canopy cover < 50%, LAI < 2)
  │   └─ YES → Use SAVI (L = 0.5)
  │
  ├─ Is your canopy very dense?
  │   (mature paddy, sugarcane, forest, LAI > 4)
  │   └─ YES → Use EVI
  │
  ├─ Is atmospheric haze/smoke a concern?
  │   (North India winter, post-harvest burning)
  │   └─ YES → Use EVI
  │
  ├─ Do you need simplicity and speed?
  │   (quick crop condition check, general monitoring)
  │   └─ YES → Use NDVI
  │
  └─ Are you comparing across seasons or years?
      └─ Stick to ONE index throughout the study
         (changing indices mid-study invalidates comparison)

For most agricultural monitoring in India:

  • June–July (Kharif sowing): SAVI — sparse canopy
  • August–September (Kharif peak): EVI or NDVI — dense canopy
  • October–November (Kharif harvest): NDVI — declining canopy, simpler
  • November–December (Rabi sowing): SAVI — sparse canopy again
  • February–March (Rabi peak): NDVI or EVI

Which Index for Which Research Application?

Research ApplicationRecommended IndexReason
District crop condition reportsNDVISimple, interpretable, widely understood
Crop yield forecasting modelEVILinear at high biomass — better predictor
Soil degradation mappingSAVISoil-sensitive — that’s what you want here
Drought early warningNDVI or EVIBoth work; NDVI more established in literature
Crop area estimationNDVIStandard in satellite crop surveys
PMFBY crop loss assessmentNDVIInsurance protocols use NDVI
Biomass/carbon stock estimationEVIBetter at high biomass ranges
Weed vs crop discriminationEVIMore sensitive to canopy differences
Agroforestry monitoringEVIMixed canopy, high LAI
Precision agriculture (field level)SAVI + NDVI combinedDifferent growth stages need different indices

Frequently Asked Questions

Q: Can I use SAVI and EVI with Landsat data instead of Sentinel-2? Yes. For Landsat 8/9: Red = Band 4, NIR = Band 5, Blue = Band 2. The same formulas apply. Resolution is 30m instead of 10m.

Q: Is there a single best vegetation index for all situations? No — and anyone who claims otherwise is oversimplifying. The right index depends on your crop, season, soil type, and research question. This post gives you the framework to choose correctly.

Q: My NDVI values look fine. Should I switch to EVI anyway? Not necessarily. If you are monitoring established crops (LAI 2–3) with NDVI and it is working — keep using it. Change only when you have a specific problem NDVI is not solving.

Q: Are there other vegetation indices beyond these three? Many — NDWI (water stress), NDRE (red edge, for Sentinel-2 Band 5), SAVI2, MSAVI, OSAVI, EVI2 (no Blue band needed), and more. I will cover NDWI and NDRE in a future post in this series.

Q: Which index does ISRO / NRSC use in their agricultural monitoring products? NRSC primarily uses NDVI for operational crop monitoring products including the Fasal programme. EVI is increasingly used in research publications. SAVI is used in specific degraded land and dryland studies.


What’s Next in the Agricultural Data Science Series


Conclusion

NDVI is a powerful tool — but it is not always the right tool. In this post you learned:

  • Why NDVI struggles with sparse vegetation and bright soils — and how SAVI fixes this
  • Why NDVI saturates at high biomass — and how EVI maintains sensitivity
  • How to calculate NDVI, SAVI, and EVI in Python from Sentinel-2 data
  • A practical decision framework for choosing the right index for each situation
  • Which index to use for specific agricultural research applications

The complete Python script above calculates all three indices, produces comparison maps, saves GeoTIFFs, and generates scatter plots showing how the indices relate to each other.

Combined with Part 1 (NDVI calculation) and Part 2 (Sentinel-2 download), you now have a complete free pipeline for vegetation monitoring — from downloading raw satellite data to producing analysis-ready vegetation index maps.


Take the Next Step

📩 Get Part 4 in your inbox — Random Forest for crop classification is coming next week. Subscribe free below and get every tutorial as soon as it is published.

👉 [Subscribe here — it’s free]

💬 Which vegetation index are you currently using in your research? Drop a comment below — I am curious how researchers across India are approaching this choice.


Data Science with DEB — Practical tutorials on Python, remote sensing, and machine learning for agricultural researchers. Published weekly at dibyendudeb.com


Leave a Comment