毫秒级的时间处理上G的图片(生成缩略图)

时间:2022-03-28 03:39:16

测试环境:

毫秒级的时间处理上G的图片(生成缩略图)

测试图片(30M):

毫秒级的时间处理上G的图片(生成缩略图)

测试计时方法:

Stopwatch sw1 = new Stopwatch();
sw1.Start();
//TODO......
sw1.Stop();
string xx = sw1.ElapsedMilliseconds.ToString();
MessageBox.Show(xx);

方法一,(1张30M图片,用时799毫秒)

public Image getThumbNailUsingGetThumbnailImage(string fileName)
{
Image img = Image.FromFile(fileName);
return img.GetThumbnailImage(, , null, IntPtr.Zero);
}

方法二,(1张30M图片,用时1329毫秒)

public Image createThumbnailUsingGDI(ref Image imgPhoto, int destWidth, int destHeight)
{
int sourceX = ;
int sourceY = ; int destX = ;
int destY = ;
int sourceWidth = imgPhoto.Width;
int sourceHeight = imgPhoto.Height; Bitmap b = new Bitmap(destWidth, destHeight); Graphics grPhoto = Graphics.FromImage(b); grPhoto.FillRectangle(Brushes.DarkGray, new Rectangle(destX, destY, destWidth, destHeight));
grPhoto.DrawLine(new Pen(Brushes.LightGray), new Point(, destHeight - ), new Point(destWidth, destHeight - ));
grPhoto.DrawLine(new Pen(Brushes.LightGray), new Point(destWidth - , ), new Point(destWidth - , destHeight));
//shade right
grPhoto.FillRectangle(Brushes.White, new Rectangle(destWidth - , , , ));
grPhoto.FillRectangle(Brushes.White, new Rectangle(destWidth - , , , ));
grPhoto.FillRectangle(Brushes.White, new Rectangle(destWidth - , , , )); //shade botton
grPhoto.FillRectangle(Brushes.White, new Rectangle(, destHeight - , , ));
grPhoto.FillRectangle(Brushes.White, new Rectangle(, destHeight - , , ));
grPhoto.FillRectangle(Brushes.White, new Rectangle(, destHeight - , , ));
grPhoto.DrawImage(imgPhoto, new Rectangle(destX + , destY + , destWidth - , destHeight - ), new Rectangle(sourceX, sourceY, sourceWidth, sourceHeight), GraphicsUnit.Pixel); grPhoto.Dispose();
return b; }

方法三,(1张30M图片,用时1636毫秒)

public Image getThumbNailWithFrame(string fileName)
{
FileStream fs = new FileStream(fileName, FileMode.Open);
Image im = Image.FromStream(fs);
Size szMax = new Size(, );
Size sz = getProportionalSize(szMax, im.Size);
// superior image quality
Bitmap bmpResized = new Bitmap(sz.Width, sz.Height);
using (Graphics g = Graphics.FromImage(bmpResized))
{
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.FillRectangle(Brushes.White, , , sz.Width, sz.Height);
int FrameWidth = ;//decides the frame border width
g.DrawRectangle(new Pen(Color.Silver, FrameWidth - ), , , sz.Width - , sz.Height - );
FrameWidth += ;//decide the frame width
g.DrawImage(im, new Rectangle(FrameWidth, FrameWidth, sz.Width - FrameWidth * , sz.Height - FrameWidth * ), new Rectangle(Point.Empty, im.Size), GraphicsUnit.Pixel);
}
im.Dispose(); im = null;
fs.Close(); fs.Dispose(); fs = null;
return bmpResized;
} private Size getProportionalSize(Size szMax, Size szReal)
{
int nWidth;
int nHeight;
double sMaxRatio;
double sRealRatio; if (szMax.Width < || szMax.Height < || szReal.Width < || szReal.Height < )
return Size.Empty; sMaxRatio = (double)szMax.Width / (double)szMax.Height;
sRealRatio = (double)szReal.Width / (double)szReal.Height; if (sMaxRatio < sRealRatio)
{
nWidth = Math.Min(szMax.Width, szReal.Width);
nHeight = (int)Math.Round(nWidth / sRealRatio);
}
else
{
nHeight = Math.Min(szMax.Height, szReal.Height);
nWidth = (int)Math.Round(nHeight * sRealRatio);
} return new Size(nWidth, nHeight);
}

方法四,(1张30M图片,用时1664毫秒)

public Image getThumbNail(string fileName)
{
FileStream fs = new FileStream(fileName, FileMode.Open);
Image im = Image.FromStream(fs);
Size szMax = new Size(, );
Size sz = getProportionalSize(szMax, im.Size);
// superior image quality
Bitmap bmpResized = new Bitmap(sz.Width, sz.Height);
using (Graphics g = Graphics.FromImage(bmpResized))
{
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.DrawImage(
im,
new Rectangle(Point.Empty, sz),
new Rectangle(Point.Empty, im.Size),
GraphicsUnit.Pixel);
}
im.Dispose(); im = null;
fs.Close(); fs.Dispose(); fs = null;
return bmpResized;
} private Size getProportionalSize(Size szMax, Size szReal)
{
int nWidth;
int nHeight;
double sMaxRatio;
double sRealRatio; if (szMax.Width < || szMax.Height < || szReal.Width < || szReal.Height < )
return Size.Empty; sMaxRatio = (double)szMax.Width / (double)szMax.Height;
sRealRatio = (double)szReal.Width / (double)szReal.Height; if (sMaxRatio < sRealRatio)
{
nWidth = Math.Min(szMax.Width, szReal.Width);
nHeight = (int)Math.Round(nWidth / sRealRatio);
}
else
{
nHeight = Math.Min(szMax.Height, szReal.Height);
nWidth = (int)Math.Round(nHeight * sRealRatio);
} return new Size(nWidth, nHeight);
}

方法五,(1张30M图片,用时735毫秒)

public Image createThumbFromProperty(string file)
{
Image image = new Bitmap(file);
Image Thumb = null;
PropertyItem[] propItems = image.PropertyItems;
foreach (PropertyItem propItem in propItems)
{
if (propItem.Id == 0x501B)
{
byte[] imageBytes = propItem.Value;
MemoryStream stream = new MemoryStream(imageBytes.Length);
stream.Write(imageBytes, , imageBytes.Length);
Thumb = Image.FromStream(stream);
break;
}
}
return Thumb;
}

方法六,(50张30M图片,237毫秒)

using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Text; //网址:https://www.experts-exchange.com/questions/21789724/How-to-create-thumbnail.html
namespace 缩略图查看
{
public class ShellThumbnail : IDisposable
{ [Flags]
private enum ESTRRET
{
STRRET_WSTR = ,
STRRET_OFFSET = ,
STRRET_CSTR =
} [Flags]
private enum ESHCONTF
{
SHCONTF_FOLDERS = ,
SHCONTF_NONFOLDERS = ,
SHCONTF_INCLUDEHIDDEN = ,
} [Flags]
private enum ESHGDN
{
SHGDN_NORMAL = ,
SHGDN_INFOLDER = ,
SHGDN_FORADDRESSBAR = ,
SHGDN_FORPARSING =
} [Flags]
private enum ESFGAO
{
SFGAO_CANCOPY = ,
SFGAO_CANMOVE = ,
SFGAO_CANLINK = ,
SFGAO_CANRENAME = ,
SFGAO_CANDELETE = ,
SFGAO_HASPROPSHEET = ,
SFGAO_DROPTARGET = ,
SFGAO_CAPABILITYMASK = ,
SFGAO_LINK = ,
SFGAO_SHARE = ,
SFGAO_READONLY = ,
SFGAO_GHOSTED = ,
SFGAO_DISPLAYATTRMASK = ,
SFGAO_FILESYSANCESTOR = ,
SFGAO_FOLDER = ,
SFGAO_FILESYSTEM = ,
SFGAO_HASSUBFOLDER = -,
SFGAO_CONTENTSMASK = -,
SFGAO_VALIDATE = ,
SFGAO_REMOVABLE = ,
SFGAO_COMPRESSED = ,
} private enum EIEIFLAG
{
IEIFLAG_ASYNC = ,
IEIFLAG_CACHE = ,
IEIFLAG_ASPECT = ,
IEIFLAG_OFFLINE = ,
IEIFLAG_GLEAM = ,
IEIFLAG_SCREEN = ,
IEIFLAG_ORIGSIZE = ,
IEIFLAG_NOSTAMP = ,
IEIFLAG_NOBORDER = ,
IEIFLAG_QUALITY =
} [StructLayout(LayoutKind.Sequential, Pack = , Size = , CharSet = CharSet.Auto)]
private struct STRRET_CSTR
{
public ESTRRET uType;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = )]
public byte[] cStr;
} [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Auto)]
private struct STRRET_ANY
{
[FieldOffset()]
public ESTRRET uType;
[FieldOffset()]
public IntPtr pOLEString;
}
[StructLayoutAttribute(LayoutKind.Sequential)]
private struct SIZE
{
public int cx;
public int cy;
} [ComImport(), Guid("00000000-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IUnknown
{ [PreserveSig()]
IntPtr QueryInterface(ref Guid riid, ref IntPtr pVoid); [PreserveSig()]
IntPtr AddRef(); [PreserveSig()]
IntPtr Release();
} [ComImportAttribute()]
[GuidAttribute("00000002-0000-0000-C000-000000000046")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
private interface IMalloc
{ [PreserveSig()]
IntPtr Alloc(int cb); [PreserveSig()]
IntPtr Realloc(IntPtr pv, int cb); [PreserveSig()]
void Free(IntPtr pv); [PreserveSig()]
int GetSize(IntPtr pv); [PreserveSig()]
int DidAlloc(IntPtr pv); [PreserveSig()]
void HeapMinimize();
} [ComImportAttribute()]
[GuidAttribute("000214F2-0000-0000-C000-000000000046")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
private interface IEnumIDList
{ [PreserveSig()]
int Next(int celt, ref IntPtr rgelt, ref int pceltFetched); void Skip(int celt); void Reset(); void Clone(ref IEnumIDList ppenum);
} [ComImportAttribute()]
[GuidAttribute("000214E6-0000-0000-C000-000000000046")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
private interface IShellFolder
{ void ParseDisplayName(IntPtr hwndOwner, IntPtr pbcReserved,
[MarshalAs(UnmanagedType.LPWStr)]string lpszDisplayName,
ref int pchEaten, ref IntPtr ppidl, ref int pdwAttributes); void EnumObjects(IntPtr hwndOwner,
[MarshalAs(UnmanagedType.U4)]ESHCONTF grfFlags,
ref IEnumIDList ppenumIDList); void BindToObject(IntPtr pidl, IntPtr pbcReserved, ref Guid riid,
ref IShellFolder ppvOut); void BindToStorage(IntPtr pidl, IntPtr pbcReserved, ref Guid riid, IntPtr ppvObj); [PreserveSig()]
int CompareIDs(IntPtr lParam, IntPtr pidl1, IntPtr pidl2); void CreateViewObject(IntPtr hwndOwner, ref Guid riid,
IntPtr ppvOut); void GetAttributesOf(int cidl, IntPtr apidl,
[MarshalAs(UnmanagedType.U4)]ref ESFGAO rgfInOut); void GetUIObjectOf(IntPtr hwndOwner, int cidl, ref IntPtr apidl, ref Guid riid, ref int prgfInOut, ref IUnknown ppvOut); void GetDisplayNameOf(IntPtr pidl,
[MarshalAs(UnmanagedType.U4)]ESHGDN uFlags,
ref STRRET_CSTR lpName); void SetNameOf(IntPtr hwndOwner, IntPtr pidl,
[MarshalAs(UnmanagedType.LPWStr)]string lpszName,
[MarshalAs(UnmanagedType.U4)] ESHCONTF uFlags,
ref IntPtr ppidlOut);
}
[ComImportAttribute(), GuidAttribute("BB2E617C-0920-11d1-9A0B-00C04FC2D6C1"), InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
private interface IExtractImage
{
void GetLocation([Out(), MarshalAs(UnmanagedType.LPWStr)]
StringBuilder pszPathBuffer, int cch, ref int pdwPriority, ref SIZE prgSize, int dwRecClrDepth, ref int pdwFlags); void Extract(ref IntPtr phBmpThumbnail);
} private class UnmanagedMethods
{ [DllImport("shell32", CharSet = CharSet.Auto)]
internal extern static int SHGetMalloc(ref IMalloc ppMalloc); [DllImport("shell32", CharSet = CharSet.Auto)]
internal extern static int SHGetDesktopFolder(ref IShellFolder ppshf); [DllImport("shell32", CharSet = CharSet.Auto)]
internal extern static int SHGetPathFromIDList(IntPtr pidl, StringBuilder pszPath); [DllImport("gdi32", CharSet = CharSet.Auto)]
internal extern static int DeleteObject(IntPtr hObject); } ~ShellThumbnail()
{
Dispose();
} private IMalloc alloc = null;
private bool disposed = false;
private Size _desiredSize = new Size(, );
private Bitmap _thumbNail; public Bitmap ThumbNail
{
get
{
return _thumbNail;
}
} public Size DesiredSize
{
get { return _desiredSize; }
set { _desiredSize = value; }
}
private IMalloc Allocator
{
get
{
if (!disposed)
{
if (alloc == null)
{
UnmanagedMethods.SHGetMalloc(ref alloc);
}
}
else
{
Debug.Assert(false, "Object has been disposed.");
}
return alloc;
}
} public Bitmap GetThumbnail(string fileName)
{
if (string.IsNullOrEmpty(fileName))
return null; if (!File.Exists(fileName) && !Directory.Exists(fileName))
{
throw new FileNotFoundException(string.Format("The file '{0}' does not exist", fileName), fileName);
}
if (_thumbNail != null)
{
_thumbNail.Dispose();
_thumbNail = null;
}
IShellFolder folder = null;
try
{
folder = getDesktopFolder;
}
catch (Exception ex)
{
throw ex;
}
if (folder != null)
{
IntPtr pidlMain = IntPtr.Zero;
try
{
int cParsed = ;
int pdwAttrib = ;
string filePath = Path.GetDirectoryName(fileName);
folder.ParseDisplayName(IntPtr.Zero, IntPtr.Zero, filePath, ref cParsed, ref pidlMain, ref pdwAttrib);
}
catch (Exception ex)
{
Marshal.ReleaseComObject(folder);
throw ex;
}
if (pidlMain != IntPtr.Zero)
{
Guid iidShellFolder = new Guid("000214E6-0000-0000-C000-000000000046");
IShellFolder item = null;
try
{
folder.BindToObject(pidlMain, IntPtr.Zero, ref iidShellFolder, ref item);
}
catch (Exception ex)
{
Marshal.ReleaseComObject(folder);
Allocator.Free(pidlMain);
throw ex;
}
if (item != null)
{
IEnumIDList idEnum = null;
try
{
item.EnumObjects(IntPtr.Zero, (ESHCONTF.SHCONTF_FOLDERS | ESHCONTF.SHCONTF_NONFOLDERS), ref idEnum);
}
catch (Exception ex)
{
Marshal.ReleaseComObject(folder);
Allocator.Free(pidlMain);
throw ex;
}
if (idEnum != null)
{
int hRes = ;
IntPtr pidl = IntPtr.Zero;
int fetched = ;
bool complete = false;
while (!complete)
{
hRes = idEnum.Next(, ref pidl, ref fetched);
if (hRes != )
{
pidl = IntPtr.Zero;
complete = true;
}
else
{
if (_getThumbNail(fileName, pidl, item))
{
complete = true;
}
}
if (pidl != IntPtr.Zero)
{
Allocator.Free(pidl);
}
}
Marshal.ReleaseComObject(idEnum);
}
Marshal.ReleaseComObject(item);
}
Allocator.Free(pidlMain);
}
Marshal.ReleaseComObject(folder);
}
return ThumbNail;
} private bool _getThumbNail(string file, IntPtr pidl, IShellFolder item)
{
IntPtr hBmp = IntPtr.Zero;
IExtractImage extractImage = null;
try
{
string pidlPath = PathFromPidl(pidl);
if (Path.GetFileName(pidlPath).ToUpper().Equals(Path.GetFileName(file).ToUpper()))
{
IUnknown iunk = null;
int prgf = ;
Guid iidExtractImage = new Guid("BB2E617C-0920-11d1-9A0B-00C04FC2D6C1");
item.GetUIObjectOf(IntPtr.Zero, , ref pidl, ref iidExtractImage, ref prgf, ref iunk);
extractImage = (IExtractImage)iunk;
if (extractImage != null)
{
Console.WriteLine("Got an IExtractImage object!");
SIZE sz = new SIZE();
sz.cx = DesiredSize.Width;
sz.cy = DesiredSize.Height;
StringBuilder location = new StringBuilder(, );
int priority = ;
int requestedColourDepth = ;
EIEIFLAG flags = EIEIFLAG.IEIFLAG_ASPECT | EIEIFLAG.IEIFLAG_SCREEN;
int uFlags = (int)flags;
try
{
extractImage.GetLocation(location, location.Capacity, ref priority, ref sz, requestedColourDepth, ref uFlags);
extractImage.Extract(ref hBmp);
}
catch (System.Runtime.InteropServices.COMException ex)
{ }
if (hBmp != IntPtr.Zero)
{
_thumbNail = Bitmap.FromHbitmap(hBmp);
}
Marshal.ReleaseComObject(extractImage);
extractImage = null;
}
return true;
}
else
{
return false;
}
}
catch (Exception ex)
{
if (hBmp != IntPtr.Zero)
{
UnmanagedMethods.DeleteObject(hBmp);
}
if (extractImage != null)
{
Marshal.ReleaseComObject(extractImage);
}
throw ex;
}
} private string PathFromPidl(IntPtr pidl)
{
StringBuilder path = new StringBuilder(, );
int result = UnmanagedMethods.SHGetPathFromIDList(pidl, path);
if (result == )
{
return string.Empty;
}
else
{
return path.ToString();
}
} private IShellFolder getDesktopFolder
{
get
{
IShellFolder ppshf = null;
int r = UnmanagedMethods.SHGetDesktopFolder(ref ppshf);
return ppshf;
}
} public void Dispose()
{
if (!disposed)
{
if (alloc != null)
{
Marshal.ReleaseComObject(alloc);
}
alloc = null;
if (_thumbNail != null)
{
_thumbNail.Dispose();
}
disposed = true;
}
}
}
}

方法七,(50张30M图片,96毫秒)

速度最快,只要兄弟们推荐的多,我下午公布源码。^_^

最快速度源码,请接收

using System;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Drawing.Drawing2D; namespace 缩略图查看
{
/// <summary>
/// Allows reading of embedded thumbnail image from the EXIF information in an image.
/// </summary>
//public abstract class ExifThumbReader
public class ExifThumbReader
{
// GDI plus functions
[DllImport("gdiplus.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
internal static extern int GdipGetPropertyItem(IntPtr image, int propid, int size, IntPtr buffer); [DllImport("gdiplus.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
internal static extern int GdipGetPropertyItemSize(IntPtr image, int propid, out int size); [DllImport("gdiplus.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
internal static extern int GdipLoadImageFromFile(string filename, out IntPtr image); [DllImport("gdiplus.dll", EntryPoint = "GdipDisposeImage", CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern int GdipDisposeImage(IntPtr image); // EXIT tag value for thumbnail data. Value specified by EXIF standard
private static int THUMBNAIL_DATA = 0x501B; /// <summary>
/// Reads the thumbnail in the given image. If no thumbnail is found, returns null
/// </summary>
//public static Image ReadThumb(string imagePath)
public Image ReadThumb(string imagePath)
{
const int GDI_ERR_PROP_NOT_FOUND = ; // Property not found error
const int GDI_ERR_OUT_OF_MEMORY = ; IntPtr hImage = IntPtr.Zero;
IntPtr buffer = IntPtr.Zero; // Holds the thumbnail data
int ret;
ret = GdipLoadImageFromFile(imagePath, out hImage); try
{
if (ret != )
throw createException(ret); int propSize; ret = GdipGetPropertyItemSize(hImage, THUMBNAIL_DATA, out propSize);
// Image has no thumbnail data in it. Return null
if (ret == GDI_ERR_PROP_NOT_FOUND)
return null;
if (ret != )
throw createException(ret); // Allocate a buffer in memory
buffer = Marshal.AllocHGlobal(propSize);
if (buffer == IntPtr.Zero)
throw createException(GDI_ERR_OUT_OF_MEMORY); ret = GdipGetPropertyItem(hImage, THUMBNAIL_DATA, propSize, buffer);
if (ret != )
throw createException(ret); // buffer has the thumbnail data. Now we have to convert it to
// an Image
return convertFromMemory(buffer);
} finally
{
// Free the buffer
if (buffer != IntPtr.Zero)
Marshal.FreeHGlobal(buffer); GdipDisposeImage(hImage);
}
} /// <summary>
/// Generates an exception depending on the GDI+ error codes (I removed some error
/// codes)
/// </summary>
private static Exception createException(int gdipErrorCode)
{
switch (gdipErrorCode)
{
case :
return new ExternalException("Gdiplus Generic Error", -);
case :
return new ArgumentException("Gdiplus Invalid Parameter");
case :
return new OutOfMemoryException("Gdiplus Out Of Memory");
case :
return new InvalidOperationException("Gdiplus Object Busy");
case :
return new OutOfMemoryException("Gdiplus Insufficient Buffer");
case :
return new ExternalException("Gdiplus Generic Error", -);
case :
return new InvalidOperationException("Gdiplus Wrong State");
case :
return new ExternalException("Gdiplus Aborted", -);
case :
return new FileNotFoundException("Gdiplus File Not Found");
case :
return new OverflowException("Gdiplus Over flow");
case :
return new ExternalException("Gdiplus Access Denied", -);
case :
return new ArgumentException("Gdiplus Unknown Image Format");
case :
return new ExternalException("Gdiplus Not Initialized", -);
case :
return new ArgumentException("Gdiplus Property Not Supported Error");
} return new ExternalException("Gdiplus Unknown Error", -);
} /// <summary>
/// Converts the IntPtr buffer to a property item and then converts its
/// value to a Drawing.Image item
/// </summary>
private static Image convertFromMemory(IntPtr thumbData)
{
propertyItemInternal prop =
(propertyItemInternal)Marshal.PtrToStructure
(thumbData, typeof(propertyItemInternal)); // The image data is in the form of a byte array. Write all
// the bytes to a stream and create a new image from that stream
byte[] imageBytes = prop.Value;
MemoryStream stream = new MemoryStream(imageBytes.Length);
stream.Write(imageBytes, , imageBytes.Length); return Image.FromStream(stream);
} /// <summary>
/// Used in Marshal.PtrToStructure().
/// We need this dummy class because Imaging.PropertyItem is not a "blittable"
/// class and Marshal.PtrToStructure only accepted blittable classes.
/// (It's not blitable because it uses a byte[] array and that's not a blittable
/// type. See MSDN for a definition of Blittable.)
/// </summary>
[StructLayout(LayoutKind.Sequential)]
private class propertyItemInternal
{
public int id = ;
public int len = ;
public short type = ;
public IntPtr value = IntPtr.Zero; public byte[] Value
{
get
{
byte[] bytes = new byte[(uint)len];
Marshal.Copy(value, bytes, , len);
return bytes;
}
}
}
}
}

测试结果:

方法 图片张数(张) 每张大小(M) 图片总大小 用时
方法一 50 30 1500M 39950毫秒=40秒
方法二 50 30 1500M 66450毫秒=66秒
方法三 50 30 1500M 81800毫秒=81秒
方法四 50 30 1500M 83200毫秒=83秒
方法五 50 30 1500M 36750毫秒=37秒
方法六 50 30 1500M 237毫秒
方法七 50 30 1500M 96毫秒