/** * @brief Build and print latlon <-> pixel transformation matrix. */ private static void BuildXformMatrix () { if (verbose) { Console.WriteLine ("assuming " + (landscape ? "landscape" : "portrait") + " orientation"); } /* * Tell each cluster what the decided orientation is, in case they are confused * (ie, at the intersection of a vertical and horizontal line). */ foreach (Cluster cluster in clusters) { cluster.SetOrientation (landscape); } /* * Find the lowest and highest lon's and lat's. */ Cluster highestLat = null; Cluster highestLon = null; Cluster lowestLat = null; Cluster lowestLon = null; foreach (Cluster cluster in clusters) { if ((cluster.CenterX == 0) && (cluster.CenterY == 0)) continue; double lat = cluster.Latitude; double lon = cluster.Longitude; if (lat != 0) { if ((highestLat == null) || (highestLat.Latitude < lat)) highestLat = cluster; if ((lowestLat == null) || (lowestLat.Latitude > lat)) lowestLat = cluster; } if (lon != 0) { if ((highestLon == null) || (highestLon.Longitude < lon)) highestLon = cluster; if ((lowestLon == null) || (lowestLon.Longitude > lon)) lowestLon = cluster; } } if (verbose) { Console.WriteLine ("raw lowestLat = " + (lowestLat == null ? "null" : lowestLat.Latitude.ToString ())); Console.WriteLine ("raw highestLat = " + (highestLat == null ? "null" : highestLat.Latitude.ToString ())); Console.WriteLine ("raw lowestLon = " + (lowestLon == null ? "null" : lowestLon.Longitude.ToString ())); Console.WriteLine ("raw highestLon = " + (highestLon == null ? "null" : highestLon.Longitude.ToString ())); } /* * If fewer than two of each complain. * But some charts only have one of one of them (two of the other), * so we can fake them by assuming 1:1 pixel ratio. */ string oneLinerValue = null; if ((lowestLat == highestLat) || (lowestLon == highestLon)) { if ((lowestLat == null) || (lowestLon == null) || ((lowestLat == highestLat) && (lowestLon == highestLon)) || !oneLiners.TryGetValue (lowestLat.Result + "," + lowestLon.Result, out oneLinerValue)) { throw new Exception ("fewer than two lats or lons found"); } } /* * Just one latitude given, make second one by using the given two longitude lines. */ if (highestLat == lowestLat) { Cluster madeUpLat = new Cluster (oneLinerValue); double f = madeUpLat.Latitude - lowestLat.Latitude; f /= highestLon.Longitude - lowestLon.Longitude; f /= Math.Cos ((lowestLat.Latitude + madeUpLat.Latitude) / 360.0 * Math.PI); int madeUpLatPixX = (int)((highestLon.CenterY - lowestLon.CenterY) * f + 0.5) + lowestLat.CenterX; int madeUpLatPixY = (int)((highestLon.CenterX - lowestLon.CenterX) * f + 0.5) + lowestLat.CenterY; madeUpLat.SetCenter (madeUpLatPixX, madeUpLatPixY); clusters.AddLast (madeUpLat); if (madeUpLat.Latitude > lowestLat.Latitude) { highestLat = madeUpLat; } else { lowestLat = madeUpLat; } } /* * Just one longitude given, make second one by using the given two latitude lines. */ if (highestLon == lowestLon) { Cluster madeUpLon = new Cluster (oneLinerValue); double f = madeUpLon.Longitude - lowestLon.Longitude; f /= highestLat.Latitude - lowestLat.Latitude; f *= Math.Cos ((lowestLat.Latitude + highestLat.Latitude) / 360.0 * Math.PI); int madeUpLonPixX = (int)((lowestLat.CenterY - highestLat.CenterY) * f + 0.5) + lowestLon.CenterX; int madeUpLonPixY = (int)((lowestLat.CenterX - highestLat.CenterX) * f + 0.5) + lowestLon.CenterY; madeUpLon.SetCenter (madeUpLonPixX, madeUpLonPixY); clusters.AddLast (madeUpLon); if (madeUpLon.Longitude > lowestLon.Longitude) { highestLon = madeUpLon; } else { lowestLon = madeUpLon; } } /* * Extract values. */ double lowestLatLat = lowestLat.Latitude; double highestLatLat = highestLat.Latitude; double lowestLonLon = lowestLon.Longitude; double highestLonLon = highestLon.Longitude; int lowestLatPixelX = lowestLat.CenterX; int highestLatPixelX = highestLat.CenterX; int lowestLonPixelX = lowestLon.CenterX; int highestLonPixelX = highestLon.CenterX; int lowestLatPixelY = lowestLat.CenterY; int highestLatPixelY = highestLat.CenterY; int lowestLonPixelY = lowestLon.CenterY; int highestLonPixelY = highestLon.CenterY; if (verbose) { Console.WriteLine (" lowestLat = " + lowestLat.Result + " = " + lowestLatLat + " at rotated (" + lowestLatPixelX + "," + lowestLatPixelY + ")"); Console.WriteLine ("highestLat = " + highestLat.Result + " = " + highestLatLat + " at rotated (" + highestLatPixelX + "," + highestLatPixelY + ")"); Console.WriteLine (" lowestLon = " + lowestLon.Result + " = " + lowestLonLon + " at rotated (" + lowestLonPixelX + "," + lowestLonPixelY + ")"); Console.WriteLine ("highestLon = " + highestLon.Result + " = " + highestLonLon + " at rotated (" + highestLonPixelX + "," + highestLonPixelY + ")"); } /* * For the rotated image: * lon = tfwA * pixx + tfwC * pixy + tfwE * lat = tfwB * pixx + tfwD * pixy + tfwF * * pixx = (tfwD * lon - tfwC * lat - tfwD * tfwE + tfwC * tfwF) / (tfwD * tfwA - tfwC * tfwB) * pixy = (tfwB * lon - tfwA * lat - tfwB * tfwE + tfwA * tfwF) / (tfwB * tfwC - tfwA * tfwD) */ double tfwA = 0, tfwB = 0, tfwC = 0, tfwD = 0, tfwE = 0, tfwF = 0; if (highestLonPixelX != lowestLonPixelX) { tfwA = (highestLonLon - lowestLonLon) / (highestLonPixelX - lowestLonPixelX); tfwE = highestLonLon - tfwA * highestLonPixelX; } if (highestLonPixelY != lowestLonPixelY) { tfwC = (highestLonLon - lowestLonLon) / (highestLonPixelY - lowestLonPixelY); tfwE = highestLonLon - tfwC * highestLonPixelY; } if (highestLatPixelX != lowestLatPixelX) { tfwB = (highestLatLat - lowestLatLat) / (highestLatPixelX - lowestLatPixelX); tfwF = highestLatLat - tfwB * highestLatPixelX; } if (highestLatPixelY != lowestLatPixelY) { tfwD = (highestLatLat - lowestLatLat) / (highestLatPixelY - lowestLatPixelY); tfwF = highestLatLat - tfwD * highestLatPixelY; } if (verbose) { Console.WriteLine ("rotated tfwA=" + tfwA); Console.WriteLine ("rotated tfwB=" + tfwB); Console.WriteLine ("rotated tfwC=" + tfwC); Console.WriteLine ("rotated tfwD=" + tfwD); Console.WriteLine ("rotated tfwE=" + tfwE); Console.WriteLine ("rotated tfwF=" + tfwF); } /* * Validate by checking that all decoded lat/lon markers can be converted to their pixel number. */ LinkedList<Cluster> latClusters = new LinkedList<Cluster> (); LinkedList<Cluster> lonClusters = new LinkedList<Cluster> (); foreach (Cluster cluster in clusters) { // verified earlier that exactly one of lat/lon is non-zero if (cluster.Latitude != 0) latClusters.AddLast (cluster); if (cluster.Longitude != 0) lonClusters.AddLast (cluster); } bool bad = false; foreach (Cluster lonClus in lonClusters) { foreach (Cluster latClus in latClusters) { double lat = latClus.Latitude; double lon = lonClus.Longitude; int pixx = latClus.CenterX | lonClus.CenterX; int pixy = latClus.CenterY | lonClus.CenterY; if (verbose) Console.WriteLine ("verifying latlon " + latClus.Result + "," + lonClus.Result + " = " + lat + "," + lon + " => rotated pixel " + pixx + "," + pixy); int comx = (int)((tfwD * lon - tfwC * lat - tfwD * tfwE + tfwC * tfwF) / (tfwD * tfwA - tfwC * tfwB) + 0.5); int comy = (int)((tfwB * lon - tfwA * lat - tfwB * tfwE + tfwA * tfwF) / (tfwB * tfwC - tfwA * tfwD) + 0.5); if (pixx == 0) pixx = comx; if (pixy == 0) pixy = comy; double clat = tfwB * pixx + tfwD * pixy + tfwF; double clon = tfwA * pixx + tfwC * pixy + tfwE; if (verbose) Console.WriteLine (" computed rotated pixel " + comx + "," + comy + " => latlon " + clat + "," + clon); comx -= pixx; comy -= pixy; clat -= lat; clon -= lon; if (comx < -VFYPIXERR || comx > VFYPIXERR) { Console.WriteLine ("horizontal verify error " + comx); bad = true; } if (comy < -VFYPIXERR || comy > VFYPIXERR) { Console.WriteLine ("vertical verify error " + comy); bad = true; } if (clat < -VFYDEGERR || clat > VFYDEGERR) { Console.WriteLine ("latitude verify error " + clat); bad = true; } if (clon < -VFYDEGERR || clon > VFYDEGERR) { Console.WriteLine ("longitude verify error " + clon); bad = true; } } } /* * Also, lat and lon pixel sizes should be the same. */ double lonperpix = Math.Abs (tfwA + tfwC); // only one of tfwA,tfwC is non-zero double latperpix = Math.Abs (tfwB + tfwD); // only one of tfwB,tfwD is non-zero double latcosine = Math.Cos ((highestLatLat + lowestLatLat) / 360.0 * Math.PI); double llperpixratio = lonperpix * latcosine / latperpix; double ratioshouldbe; if (!notsquare.TryGetValue (csvoutid, out ratioshouldbe)) { ratioshouldbe = 1.0; } if (verbose) { Console.WriteLine ("lon per pix=" + lonperpix + ", lat per pix=" + latperpix + ", ratio=" + llperpixratio + ", expecting=" + ratioshouldbe); } llperpixratio /= ratioshouldbe; if (llperpixratio > 1.0) llperpixratio = 1.0 / llperpixratio; if (llperpixratio < VFYPIXRAT) { Console.WriteLine ("pixels are not square"); bad = true; } if (bad) throw new Exception ("latlon <-> rotated pixel verify error"); /* * Now we have: * * Mr * TCAr * Rc * TACo * Po = L * * Mr = [ tfwA tfwC tfwE ] the TFW matrix from above calcuations * [ tfwB tfwD tfwF ] that will transform a rotated pixel * [ 0 0 1 ] into lat/lon * * TCAr = [ 1 0 Wr/2 ] translate center-relative to absolue in the rotated image * [ 0 1 Hr/2 ] * [ 0 0 1 ] * * Rc = [ cos th sin th 0 ] rotates a center-relative pixel in the original image * [ -sin th cos th 0 ] to corresponding center-relative pixel in rotated image * [ 0 0 1 ] * * TACo = [ 1 0 -Wo/2 ] translate absolute to center-relative in the original image * [ 0 1 -Ho/2 ] * [ 0 0 1 ] * * Po = [ Xo ] an arbitrary pixel in the original image * [ Yo ] * [ 1 ] * * L = [ lon ] the corresponding lat/lon coordinate in the real world * [ lat ] * [ 1 ] */ double[,] TACo = new double[3,3] { { 1, 0, -origWidth / 2.0 }, { 0, 1, -origHeight / 2.0 }, { 0, 0, 1 } }; double denom = Math.Sqrt (latLonSlopeX * latLonSlopeX + latLonSlopeY * latLonSlopeY); double costh = latLonSlopeX / denom; double sinth = latLonSlopeY / denom; double[,] Rc = new double[3,3] { { costh, sinth, 0 }, { -sinth, costh, 0 }, { 0, 0, 1 } }; double[,] TCAr = new double[3,3] { { 1, 0, portWidth / 2.0 }, { 0, 1, portHeight / 2.0 }, { 0, 0, 1 } }; double[,] Mr = new double[3,3] { { tfwA, tfwC, tfwE }, { tfwB, tfwD, tfwF }, { 0, 0, 1 } }; /* * And so we can compute: * * Mo = Mr * TCAr * Rc * TACo * * Such that: * * Mo * Po = L */ double[,] Mo = MatMul (MatMul (Mr, TCAr), MatMul (Rc, TACo)); /* * Extract TFW values that convert a pixel in the original image * directly to the corresponding latitude/longitude. */ tfwA = Mo[0,0]; tfwB = Mo[1,0]; tfwC = Mo[0,1]; tfwD = Mo[1,1]; tfwE = Mo[0,2]; tfwF = Mo[1,2]; if (verbose) { Console.WriteLine ("final tfwA=" + tfwA); Console.WriteLine ("final tfwB=" + tfwB); Console.WriteLine ("final tfwC=" + tfwC); Console.WriteLine ("final tfwD=" + tfwD); Console.WriteLine ("final tfwE=" + tfwE); Console.WriteLine ("final tfwF=" + tfwF); } /* * Calculate inverse TFW values to convert a lat/lon to a pixel in * the original image. * * Compute: * MoInv = 1 / Mo * * Given from above: * Mo * Po = L * * Multiply both sides by MoInv: * MoInv * Mo * Po = MoInv * L * * And we get: * Po = MoInv * L * * pixx = wftA * lon + wftC * lat + wftE * pixy = wftB * lon + wftD * lat + wftF */ double[,] MoInv = MatInv (Mo); double wftA = MoInv[0,0]; double wftB = MoInv[1,0]; double wftC = MoInv[0,1]; double wftD = MoInv[1,1]; double wftE = MoInv[0,2]; double wftF = MoInv[1,2]; if (verbose) { Console.WriteLine ("final wftA=" + wftA); Console.WriteLine ("final wftB=" + wftB); Console.WriteLine ("final wftC=" + wftC); Console.WriteLine ("final wftD=" + wftD); Console.WriteLine ("final wftE=" + wftE); Console.WriteLine ("final wftF=" + wftF); } /* * Maybe write to .csv file. * Lines are of the form: * csvoutid,tfwA,tfwB,tfwC,tfwD,tfwE,tfwF,wftA,wftB,wftC,wftD,wftE,wftF */ if ((csvoutfile != null) && (csvoutid != null)) { StringBuilder sb = new StringBuilder (); sb.Append (csvoutid); sb.Append (','); sb.Append (tfwA); sb.Append (','); sb.Append (tfwB); sb.Append (','); sb.Append (tfwC); sb.Append (','); sb.Append (tfwD); sb.Append (','); sb.Append (tfwE); sb.Append (','); sb.Append (tfwF); sb.Append (','); sb.Append (wftA); sb.Append (','); sb.Append (wftB); sb.Append (','); sb.Append (wftC); sb.Append (','); sb.Append (wftD); sb.Append (','); sb.Append (wftE); sb.Append (','); sb.Append (wftF); sb.Append ('\n'); File.WriteAllText (csvoutfile, sb.ToString ()); } /* * Maybe create marked-up .png file. */ if (markedpng != null) { /* * Get original image with white background. */ bmp = new Bitmap (pngname); width = bmp.Width; height = bmp.Height; for (int y = 0; y < height; y ++) { for (int x = 0; x < width; x ++) { Color c = bmp.GetPixel (x, y); int w = 255 - c.A; if (w != 0) { bmp.SetPixel (x, y, Color.FromArgb (c.R + w, c.G + w, c.B + w)); } } } /* * Draw red crosses wherever lat/lon lines intersect. */ foreach (Cluster lonClus in lonClusters) { foreach (Cluster latClus in latClusters) { double lat = latClus.Latitude; double lon = lonClus.Longitude; int x = (int)(wftA * lon + wftC * lat + wftE + 0.5); int y = (int)(wftB * lon + wftD * lat + wftF + 0.5); DrawCross (x, y, 25, 2, Color.Red); } } bmp.Save (markedpng); } }