Introduction: The Index Problem Nobody Talks About
Page contents
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:
| Index | Formula | Best For | Avoid When |
|---|---|---|---|
| NDVI | (NIR – Red) / (NIR + Red) | General crop monitoring, established canopy | Sparse vegetation, very dense canopy |
| SAVI | (NIR – Red) / (NIR + Red + L) × (1 + L) | Sparse vegetation, early season, dryland farming, semi-arid areas | Dense canopy (use EVI instead) |
| EVI | G × (NIR – Red) / (NIR + C1×Red – C2×Blue + L) | Dense canopy, areas with atmospheric haze, high biomass | Simple 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 Situation | Use SAVI? | Reason |
|---|---|---|
| Early Kharif season (June–July) | ✅ Yes | Sparse canopy, soil dominant |
| Dryland farming (Rajasthan, Vidarbha) | ✅ Yes | Bright soils, low canopy density |
| Rabi wheat early stage (Nov–Dec) | ✅ Yes | Sparse seedling stage |
| Degraded pasture assessment | ✅ Yes | Mixed soil/sparse grass |
| Semi-arid horticulture (mango orchards, early stage) | ✅ Yes | Inter-row soil visible |
| Peak Kharif paddy (Aug–Sep) | ❌ No | Dense canopy — use NDVI or EVI |
| Forest monitoring | ❌ No | High 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 Value | Vegetation Status |
|---|---|
| > 0.5 | Dense, healthy vegetation |
| 0.3 – 0.5 | Moderate vegetation |
| 0.1 – 0.3 | Sparse vegetation |
| 0.0 – 0.1 | Very sparse / bare soil |
| < 0.0 | Non-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 Situation | Use EVI? | Reason |
|---|---|---|
| Peak season dense paddy (Aug–Sep, Assam) | ✅ Yes | Avoids NDVI saturation |
| Dense sugarcane monitoring | ✅ Yes | Very high biomass crop |
| Forest and agroforestry assessment | ✅ Yes | High canopy, saturation issue |
| North India winter (haze season) | ✅ Yes | Atmospheric correction built in |
| Biomass estimation studies | ✅ Yes | Linear response at high LAI |
| Early season sparse crops | ❌ Marginal | SAVI is simpler and equally good |
| Simple, quick crop health check | ❌ No | NDVI 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:
- 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.
- 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.
- 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 Application | Recommended Index | Reason |
|---|---|---|
| District crop condition reports | NDVI | Simple, interpretable, widely understood |
| Crop yield forecasting model | EVI | Linear at high biomass — better predictor |
| Soil degradation mapping | SAVI | Soil-sensitive — that’s what you want here |
| Drought early warning | NDVI or EVI | Both work; NDVI more established in literature |
| Crop area estimation | NDVI | Standard in satellite crop surveys |
| PMFBY crop loss assessment | NDVI | Insurance protocols use NDVI |
| Biomass/carbon stock estimation | EVI | Better at high biomass ranges |
| Weed vs crop discrimination | EVI | More sensitive to canopy differences |
| Agroforestry monitoring | EVI | Mixed canopy, high LAI |
| Precision agriculture (field level) | SAVI + NDVI combined | Different 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
- Part 4: Random Forest for crop type classification using Sentinel-2 multispectral data
- Part 5: Time series NDVI analysis for paddy crop monitoring — a complete case study from Assam
- Part 6: Predicting crop yield from satellite indices using Python regression models
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