<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Liang Jacky</title>
    <description>The latest articles on DEV Community by Liang Jacky (@liangjacky8921).</description>
    <link>https://dev.to/liangjacky8921</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4179095%2F6bfcef86-7023-429d-b27d-5b2e71a41c01.jpg</url>
      <title>DEV Community: Liang Jacky</title>
      <link>https://dev.to/liangjacky8921</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/liangjacky8921"/>
    <language>en</language>
    <item>
      <title>FieldLens: Orchard Vigor Monitoring That a Grower Would Actually Pay For — Sentinel-2 NDVI + Local Gemma</title>
      <dc:creator>Liang Jacky</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:28:34 +0000</pubDate>
      <link>https://dev.to/liangjacky8921/fieldlens-orchard-vigor-monitoring-that-a-grower-would-actually-pay-for-sentinel-2-ndvi-local-4i0o</link>
      <guid>https://dev.to/liangjacky8921/fieldlens-orchard-vigor-monitoring-that-a-grower-would-actually-pay-for-sentinel-2-ndvi-local-4i0o</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;FieldLens turns a drawn orchard boundary into plain-language growing advice.&lt;/p&gt;

&lt;p&gt;A grower draws a plot on the map. FieldLens pulls Sentinel-2 imagery through Google Earth Engine, computes an NDVI vigor time series for that plot, and a locally running Gemma model turns the curve into a short report: what changed, what it might mean, and what to check in the field. (Where no local model is available, a deterministic rule engine covers the same steps — labeled in-app as a fallback.)&lt;/p&gt;

&lt;p&gt;We're starting with high-value orchards in Meizhou — Dapu honey pomelo and citrus — at 199 yuan per plot per year. The buyer is not a tech company. It's a grower who already walks the orchard and wants to know &lt;em&gt;which block to check first&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Why would a grower pay for this when Sentinel-2 data is free? Because they're not buying data — they're buying a shortcut: instead of walking every block to find the struggling one, the report tells them which block to check first. The 199 yuan isn't priced on our costs (near zero per report, thanks to the local model). It's priced on the grower's decision: cheap enough to say yes to without a meeting.&lt;/p&gt;

&lt;p&gt;What the report deliberately won't do: diagnose a disease, or prescribe pesticide doses. It says what the NDVI curve shows, flags what looks unusual, and tells the grower to go check in person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Live demo: &lt;a href="https://fieldlens-api.onrender.com/" rel="noopener noreferrer"&gt;https://fieldlens-api.onrender.com/&lt;/a&gt; — the tool page is at &lt;code&gt;/app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note: the demo runs on Render's free tier — if it has been idle a while, the first load may take ~30 seconds to wake up; everything after that is fast.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6gfw8iq903xiy3585l3u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6gfw8iq903xiy3585l3u.gif" alt="Demo run: load the sample field and generate the report, ~30 seconds" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fju11nho4u5jb871wke9k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fju11nho4u5jb871wke9k.png" alt="Landing page" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fddxbz76tm7wdkhes60bo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fddxbz76tm7wdkhes60bo.png" alt="Empty tool state" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8xiwrrtztl0n0axevay9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8xiwrrtztl0n0axevay9.png" alt="Sample field loaded — 13 cloud-masked scenes, Watch" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faxgc2q0obuziz8l9nm4c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faxgc2q0obuziz8l9nm4c.png" alt="Advisory: October note, Variable trend, three 7-day actions" width="549" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fph2901fj3cg0e2cq3p7b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fph2901fj3cg0e2cq3p7b.png" alt="Same report in Chinese" width="549" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2oviobrp1ezgbdfz8u3h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2oviobrp1ezgbdfz8u3h.png" alt="Every threshold traces to a published source (R1–R6)" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy4son6b8kjmd9539e3z0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy4son6b8kjmd9539e3z0.png" alt="Same flow on a 375px phone" width="800" height="1732"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How to try it in 30 seconds: open the demo, click "Load demo field", then generate the report. No account, no drawing required.&lt;/p&gt;

&lt;p&gt;An honest note on scope: the live demo above runs the UI and the sample field on a transparent rule-engine fallback (labeled in-app as "Rule-based fallback"); the full GEE + local Gemma chain runs on my own machine — setup is in the README. The sample polygon is illustrative, not a real farm's boundary, and every threshold is a literature value cited in the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/liangjacky8921/fieldlens" rel="noopener noreferrer"&gt;https://github.com/liangjacky8921/fieldlens&lt;/a&gt; (Public)&lt;/li&gt;
&lt;li&gt;The README covers setup (&lt;code&gt;ollama pull gemma2:2b&lt;/code&gt;, &lt;code&gt;earthengine authenticate&lt;/code&gt;), the &lt;code&gt;/api/ndvi&lt;/code&gt; endpoint, and a data-sources section with a source note on every threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Frontend: map drawing (Leaflet) + bilingual UI (English default, Chinese reports for real growers).&lt;/li&gt;
&lt;li&gt;NDVI: NDVI = (B8 − B4) / (B8 + B4) on COPERNICUS/S2_SR_HARMONIZED, Sentinel-2, ~5-day revisit.&lt;/li&gt;
&lt;li&gt;Interpretation thresholds (all sourced in README, not invented):

&lt;ul&gt;
&lt;li&gt;NDVI ≥ 0.70 = high-vigor canopy (published sweet-orange study).&lt;/li&gt;
&lt;li&gt;NDVI &amp;gt; 0.25 = vegetation vs soil (published sooty-mould study).&lt;/li&gt;
&lt;li&gt;Citrus-orchard NDVI ranges from published studies, including how low values can go under stress or infection — the report treats a drop as a reason to go look, not a diagnosis.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Report: local Gemma (&lt;code&gt;gemma2:2b&lt;/code&gt; via Ollama) with a &lt;code&gt;lang&lt;/code&gt; parameter (English default, Chinese for growers). Alert rules (e.g. drop &amp;gt; 0.10) are project settings, labeled as such — not literature values.&lt;/li&gt;
&lt;li&gt;Sample field: an illustrative polygon inside the real Dapu honey-pomelo geographical-indication protection zone (116°18′–116°56′E, 24°01′–24°41′N). It's labeled "Sample field" in the app — not a real farm's boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The NDVI pipeline is one file (&lt;code&gt;app/gee_ndvi.py&lt;/code&gt;). Per-scene mean is computed twice: once with QA60 cloud masking, and once unmasked as a fallback for scenes where cloud cover masks the whole plot. Scene-level means are then reduced to a single value per revisit date, and any scene with a null (fully cloud-masked) mean is dropped rather than counted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Dual-track per-scene NDVI mean: QA60-masked, falling back to unmasked mean
# so a fully cloud-covered scene still contributes instead of being silently dropped.
&lt;/span&gt;&lt;span class="n"&gt;masked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ndvi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduceRegion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reducer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reducer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;unmasked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ndvi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unmask&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;reduceRegion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reducer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reducer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;geometry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;geom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Algorithms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;If&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Algorithms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IsEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;masked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;unmasked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;masked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nd&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Open Innovation Matters
&lt;/h2&gt;

&lt;p&gt;All economic, not ideological:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The unit economics only work because the model is local.&lt;/strong&gt; A per-report cloud API bill would eat the 199 yuan/plot/year price. Local Gemma has no per-call charge, so the price can stay low enough for a grower to say yes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The data stays on the farm.&lt;/strong&gt; Plot boundaries and orchard health are business-sensitive. Nothing is uploaded to a vendor server for inference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open data + open weights = auditable numbers.&lt;/strong&gt; Sentinel-2 is open Copernicus data; every threshold in the README carries its source. A grower (or a judge) can check where each number came from.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  First User
&lt;/h2&gt;

&lt;p&gt;We're in talks with a Meizhou pomelo cooperative to run an on-site validation in November — real blocks, real walking, comparing the report's "check this block first" ranking against the grower's own walk order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — report generation runs on local Gemma 2B via Ollama; the live demo surfaces the labeled fallback path because Ollama can't run on Render's free tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt; — frontend + FastAPI backend + GEE-backed NDVI pipeline deployed on Render; &lt;code&gt;/health&lt;/code&gt; reports &lt;code&gt;gee_ready: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overall (Week 2: Side Hustle)&lt;/strong&gt; — a grower would actually pay for this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr9kmmw80u15y13x02yla.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr9kmmw80u15y13x02yla.gif" alt=" " width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
