I'm writing a .NET 4 application that imports and saves images for printing. It's important that the saved images resolution (DPI not pixel dimensions) be set to the value we specify so they print correctly.
Some of the images we import come without the resolution value (bad EXIF when they were generated), so we have to correct that before writing them. We use Bitmap.SetResolution for that. It works fine on XP and Windows 8, but when we write (Bitmap.Save) the images on Windows 7, they are always written with the original resolution meta info, ignoring SetResolution.
Here's a test we made, works on XP and 8, not on 7.
string originalFile = #"D:\temp\img\original_img.jpg";
string newFile = #"D:\temp\img\new_img.jpg";
Bitmap bitmap = (Bitmap)Image.FromFile(originalFile);
bitmap.SetResolution(200, 200);
bitmap.Save(newFile, ImageFormat.Jpeg);
Image image = Image.FromFile(newFile);
int dpiX = (int)Math.Round(image.HorizontalResolution, MidpointRounding.ToEven);
int dpiY = (int)Math.Round(image.VerticalResolution, MidpointRounding.ToEven);
Console.WriteLine("DPI is {0} x {1}", dpiX, dpiY);
Before saving, debug always shows the correct resolution assigned by SetResolution, the saved image is where the problem is.
This is probably what was reported here:
http://social.msdn.microsoft.com/Forums/vstudio/en-US/62368caa-05f4-4798-9c59-5d82f881a97c/systemdrawingbitmapsetresolution-is-completely-broken-on-windows-7?forum=netfxbcl
But the issue there seems to remain unsolved. Is there really no way to just make it work? Do I have to use extra libraries for this?
I've found a workaround that will do the job. It's not elegant but...
Instead of applying the resolution to the original image, make a copy of it and work on the copy:
Bitmap bitmap = (Bitmap)Image.FromFile(originalFile);
Bitmap newBitmap = new Bitmap(bitmap)
newBitmap.SetResolution(200, 200);
newBitmap.Save(newFile, ImageFormat.Jpeg);
Now it works on Windows 7. Go figure.
I like Hans Passant's idea, though, it's cleaner. I don't know if what I did messes up with the image, if there is recompression or not.
Hmya, this is a bug in a Windows component. The Windows group is always very reluctant to get bugs like this fixed, breaking changes are postponed to a next Windows version. It did get fixed in Windows 8. Do consider how unusual it is what you are doing, the DPI of an image should always be set by the device that recorded the image. Like the camera or scanner, they never get this wrong. There just isn't any device around that has a 200 dots-per-inch resolution.
If you are desperate enough to find a workaround then you could consider patching the file itself. Not hard to do for a JPEG file, the fields in the file header are pretty easy to get to:
using System.IO;
...
public static void SetJpegResolution(string path, int dpi) {
using (var jpg = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
using (var br = new BinaryReader(jpg)) {
bool ok = br.ReadUInt16() == 0xd8ff; // Check header
ok = ok && br.ReadUInt16() == 0xe0ff;
br.ReadInt16(); // Skip length
ok = ok && br.ReadUInt32() == 0x4649464a; // Should be JFIF
ok = ok && br.ReadByte() == 0;
ok = ok && br.ReadByte() == 0x01; // Major version should be 1
br.ReadByte(); // Skip minor version
byte density = br.ReadByte();
ok = ok && (density == 1 || density == 2);
if (!ok) throw new Exception("Not a valid JPEG file");
if (density == 2) dpi = (int)Math.Round(dpi / 2.56);
var bigendian = BitConverter.GetBytes((short)dpi);
Array.Reverse(bigendian);
jpg.Write(bigendian, 0, 2);
jpg.Write(bigendian, 0, 2);
}
}
Related
I am using the following render code. This is a slightly modified code because, in the actual code, I am getting all the files from the FTP server. CurrentWindowCenter and CurrentWindowWidth hold the current value of WL and WW. This value can be changed from the UI. I am showing the rendered WritableBitmap directly to the user using an image component inside a canvas.
But the rendering of the image is very slow. Especially for images with large file sizes such as X-Ray. So, the WW and WL change is also very slow since it also uses the render function.
I am not very knowledgeable about this. But is there a way to make the rendering or WW/WL change faster? Is there a way to skip the rendering of the image every time a WW/WL change happens?
Any advice in the right direction is appreciated.
Thanks in advance.
// assume filePath holds an actual file location.
var filePath = "";
var dicomFile = new DicomFile.Open(filePath);
var dicomImage = new DicomImage(dicomFile.DataSet);
if (CurrentWindowCenter.HasValue && CurrentWindowWidth.HasValue)
{
dicomImage.WindowCenter = Convert.ToDouble(CurrentWindowCenter.Value);
dicomImage.WindowWidth = Convert.ToDouble(CurrentWindowWidth.Value);
}
dicomImage.RenderImage().AsWriteableBitmap();
Environment
Fo-Dicom (4.0.8)
.NET Framework 4.8
WPF
My guess is that fo-dicom is not really intended for high performance and more for compatibility. For best performance you should probably use the GPU via DirectX, OpenCL or similar. Second best should be some tightly optimized SIMD code, probably using c++.
But there might be some improvements to be had using just c#. From the looks of it fo-dicom creates a new image, copies pixels to this image, and then creates a writeableBitmap and does another copy. These step will take some extra time.
My code for copying pixels and applying a lut/transfer function look like this:
public static unsafe void Mono16ToMono8(
byte* source,
byte* target,
int sourceStride,
int targetStride,
int width,
int height,
byte[] lut)
{
Parallel.For(0, height, y =>
{
var ySource = source + y * sourceStride;
var yTarget = target + y * targetStride;
var srcUshort = (ushort*)ySource;
for (int x = 0; x < width; x++)
{
var sample = srcUshort[x];
yTarget[x] = lut[sample];
}
});
}
And the code to do the actual update of the writeable bitmap:
public static unsafe void Update(
this WriteableBitmap self,
IImage source,
byte[] lut)
{
self.Lock();
try
{
var targetPtr = (byte*)self.BackBuffer;
fixed (byte* sourcePtr = source.Data)
{
if (source.PixelFormat == PixelType.Mono16)
{
Mono16ToMono8(
sourcePtr,
targetPtr,
source.Stride,
self.BackBufferStride,
source.Width,
source.Height,
lut);
}
}
self.AddDirtyRect(new Int32Rect(0, 0, (int)self.Width, (int)self.Height));
}
finally
{
self.Unlock();
}
}
This uses an internal IImage format, where source.Data is a ReadOnlySpan<byte>, but could just as well be a byte[]. I hope most of the other properties are self-explanatory. I would expect this code to be a bit faster since it avoids both allocations and some copying steps.
All of this assumes the image is in 16-bit unsigned format, that is common for dicom, but not the only format. It also assumes you can actually get a hold of a pointer to the actual pixel-buffer, and an array of the lut that maps each possible pixelvalue to a byte. It also assumes a writeablebitmap of the correct size and color space.
And as previously mentioned, if you want both high performance, and handle all possible image formats, you might need to invest time to build your own image rendering pipeline.
I'm decoding whatever the camera codec is and then always encode it to H264 and more specifically qsv if it is supported. Currently having 2 cameras to test. One is H264 encoding and one is rawvideo. The problem comes with rawvideo. The pixel format is BGR24 and scaling to NV12
I will simplify the code because it is as any other example.
avcodec_send_packet()
// while
avcodec_receive_frame()
// if frame is not EAGAIN convert BGR24 to NV12
if (_pConvertContext == null)
{
_pConvertContext = CreateContext(sourcePixFmt, targePixFmt);
}
if (_convertedFrameBufferPtr == IntPtr.Zero)
{
int buffSize = ffmpeg.av_image_get_buffer_size(targePixFmt, sourceFrame->width, sourceFrame->height, 1);
_convertedFrameBufferPtr = Marshal.AllocHGlobal(buffSize);
ffmpeg.av_image_fill_arrays(ref _dstData, ref _dstLinesize, (byte*)_convertedFrameBufferPtr, targePixFmt, sourceFrame->width, sourceFrame->height, 1);
}
return ScaleImage(_pConvertContext, sourceFrame, targePixFmt, _dstData, _dstLinesize);
And ScaleImage method
ffmpeg.sws_scale(ctx, sourceFrame->data, sourceFrame->linesize, 0, sourceFrame->height, dstData, dstLinesize);
AVFrame* f = ffmpeg.av_frame_alloc();
var data = new byte_ptrArray8();
data.UpdateFrom(dstData);
var linesize = new int_array8();
linesize.UpdateFrom(dstLinesize);
f->data = data;
f->linesize = linesize;
f->width = sourceFrame->width;
f->height = sourceFrame->height;
f->format = (int)targePixelFormat;
return f;
After that sending the scaled frame to the encoder and receiving it and writing the file. After that I call av_frame_free(&frame) on the frame returned from the method. But when I set breakpoint I can see the address of the frame is the same even after calling av_frame_alloc() and cleaning everytime. And I think this is the reason for the memory leak. If I do deep clone of the f before returning it everything is fine. Why is that happening while the same logic works fine with the other camera?
Well, after 3 lost days of trying to figure out why it happens with certain cameras only, I've made a memory dump using DebugDiag 2 with a memory leak tracker. It turned out there is tons of memory allocations by _aligned_offset_malloc as you can see in the picture below. I was using ffmpeg 4.2.2, downgrading to 4.0.2 fixed the problem for me. No memory leak now. I'm using 32bit version.
I have reworded my question from the previous one. I have created a Desktop recorder and it works perfectly except one thing. When I try to encode the video and place the media in my output folder C:\Videos it throws an exception.
Keep in mind that the output .xesc will save to the Videos folder on the C drive. However when i try to convert it to .wmv format it throws the following exception.
An unhandled exception of type Microsoft.Expression.Encoder.InvalidMediaFileException occured in Microsoft.Expression.Encoder.dll Aditional Information: Access Denied (Exception from HRESULT: 0x80070005(E_AccessDenied))
I have posted the encoder below.
**** Source Code ******
Here is the source code dealing with the encoding. I was working on a few things on it and if you see any mistakes or anything to better it then let me know. It works perfectly and puts the .xesc formatbut it will not save the .wmv
void Encode(string jobPath)
{
using (Job j = new Job())
{
MediaItem mediaItem = new MediaItem(Environment.GetFolderPath(Environment.SpecialFolder.MyVideos) + #"\IvanSoft Desktop Recorder");
var size = mediaItem.OriginalVideoSize;
WindowsMediaOutputFormat WMV_Format = new WindowsMediaOutputFormat();
WMV_Format.VideoProfile = new Microsoft.Expression.Encoder.Profiles.AdvancedVC1VideoProfile();
WMV_Format.AudioProfile = new Microsoft.Expression.Encoder.Profiles.WmaAudioProfile();
WMV_Format.VideoProfile.AspectRatio = new System.Windows.Size(16, 9);
WMV_Format.VideoProfile.AutoFit = true;
if (size.Width >= 1920 && size.Height >= 1080)
{
WMV_Format.VideoProfile.Size = new System.Drawing.Size(1920, 1080);
WMV_Format.VideoProfile.Bitrate = new Microsoft.Expression.Encoder.Profiles.VariableUnconstrainedBitrate(6000);
}
else if (size.Width >= 1280 && size.Height >= 720)
{
WMV_Format.VideoProfile.Size = new System.Drawing.Size(1280, 720);
WMV_Format.VideoProfile.Bitrate = new Microsoft.Expression.Encoder.Profiles.VariableUnconstrainedBitrate(4000);
}
else
{
WMV_Format.VideoProfile.Size = new System.Drawing.Size(size.Width, size.Height);
WMV_Format.VideoProfile.Bitrate = new Microsoft.Expression.Encoder.Profiles.VariableUnconstrainedBitrate(2000);
}
mediaItem.VideoResizeMode = VideoResizeMode.Letterbox;
mediaItem.OutputFormat = WMV_Format;
j.MediaItems.Add(mediaItem);
j.CreateSubfolder = false;
j.OutputDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos) + #"\IvanSoft Desktop Recorder.xesc";
j.EncodeProgress += new EventHandler<EncodeProgressEventArgs>(j_EncodeProgress);
j.Encode();
}
}
This is not necessary a problem because i can convert the .xesc manually but that takes time. I would like it to work when i press Save_btnClik It will encode like its supposed to. This happens in win8.1 and win10.
What kind of permission do i need to gain access?
Why you dont use "SaveFileDiaLog". I think it is the best method. You can find it in "Toolbox" in Winform_application Visual studio
Ok I found out that Microsoft Expression Encoder 4 does not truely save a file to a .wmv format. It only changes the .xesc to a .wmv.
I found this out by once I got it converted to what I thought was a .wmv I loaded it to Movie Maker and it said that .xesc was not supported.
So the ultimate outcome is that MEE4 encoder will produce a .xesc format. Then I also noticed that third party converters that truely convert the file. The video and sound is not synced. So unless I missed a piece of code somewhere, third party converters are not the way to go.
So I will have to figure a way to truly convert .xesc to another format and retain frame speed and sync.
How to read a tiff file's dimension (width and height) and resolution (horizontal and vertical) without first loading it into memory by using code like the following. It is too slow for big files and I don't need to manipulate them.
Image tif = Image.FromFile(#"C:\large_size.tif");
float width = tif.PhysicalDimension.Width;
float height = tif.PhysicalDimension.Height;
float hresolution = tif.HorizontalResolution;
float vresolution = tif.VerticalResolution;
tif.Dispose();
Edit:
Those tiff files are Bilevel and have a dimension of 30x42 inch. The file sizes are about 1~2 MB. So the method above works Ok but slow.
Ran into this myself and found the solution (possibly here). Image.FromStream with validateImageData = false allows you access to the information you're looking for, without loading the whole file.
using(FileStream stream = new FileStream(#"C:\large_size.tif", FileMode.Open, FileAccess.Read))
{
using(Image tif = Image.FromStream(stream, false, false))
{
float width = tif.PhysicalDimension.Width;
float height = tif.PhysicalDimension.Height;
float hresolution = tif.HorizontalResolution;
float vresolution = tif.VerticalResolution;
}
}
As far as I know, all classes from System.Drawing namespace load image data immediately when image is open.
I think LibTiff.Net can help you to read image properties without loading image data. It's free and open-source (BSD license, suitable for commercial applications).
Here is a sample for your task (error checks are omitted for brevity):
using BitMiracle.LibTiff.Classic;
namespace ReadTiffDimensions
{
class Program
{
static void Main(string[] args)
{
using (Tiff image = Tiff.Open(args[0], "r"))
{
FieldValue[] value = image.GetField(TiffTag.IMAGEWIDTH);
int width = value[0].ToInt();
value = image.GetField(TiffTag.IMAGELENGTH);
int height = value[0].ToInt();
value = image.GetField(TiffTag.XRESOLUTION);
float dpiX = value[0].ToFloat();
value = image.GetField(TiffTag.YRESOLUTION);
float dpiY = value[0].ToFloat();
}
}
}
}
Disclaimer: I am one of the maintainers of the library.
Try this, it seems to be what you are looking for. Just skip everything after:
TIFFGetField(tif, TIFFTAG_IMAGEWIDTH, ref w); //your width
TIFFGetField(tif, TIFFTAG_IMAGELENGTH, ref h); //your height
TIFFGetField(tif, TIFFTAG_BITSPERSAMPLE, ref bits);
TIFFGetField(tif, TIFFTAG_SAMPLESPERPIXEL, ref samples);
Don't forget to close after you:
TIFFClose(tif);
The only way I can think of is reading the tiff binary header.
Here you can download the specification: http://partners.adobe.com/public/developer/tiff/index.html
Here is some code used to read Tiffs that you can use to learn:
http://www.koders.com/csharp/fidF6632006F25B8E5B3BCC62D13076B38D71847929.aspx?s=zoom
I created a library to read the tiff headers some time ago (with this two resources as base) but it was part of my employer code so I can't post my code here and I can say it is no really hard.
I Hope this helps.
In windows XP "FileInfo.LastWriteTime" will return the date a picture is taken - regardless of how many times the file is moved around in the filesystem.
In Vista it instead returns the date that the picture is copied from the camera.
How can I find out when a picture is taken in Vista? In windows explorer this field is referred to as "Date Taken".
Here's as fast and clean as you can get it. By using FileStream, you can tell GDI+ not to load the whole image for verification. It runs over 10 × as fast on my machine.
//we init this once so that if the function is repeatedly called
//it isn't stressing the garbage man
private static Regex r = new Regex(":");
//retrieves the datetime WITHOUT loading the whole image
public static DateTime GetDateTakenFromImage(string path)
{
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
using (Image myImage = Image.FromStream(fs, false, false))
{
PropertyItem propItem = myImage.GetPropertyItem(36867);
string dateTaken = r.Replace(Encoding.UTF8.GetString(propItem.Value), "-", 2);
return DateTime.Parse(dateTaken);
}
}
And yes, the correct id is 36867, not 306.
The other Open Source projects below should take note of this. It is a huge performance hit when processing thousands of files.
I maintained a simple open-source library since 2002 for extracting metadata from image/video files.
// Read all metadata from the image
var directories = ImageMetadataReader.ReadMetadata(stream);
// Find the so-called Exif "SubIFD" (which may be null)
var subIfdDirectory = directories.OfType<ExifSubIfdDirectory>().FirstOrDefault();
// Read the DateTime tag value
var dateTime = subIfdDirectory?.GetDateTime(ExifDirectoryBase.TagDateTimeOriginal);
In my benchmarks, this code runs over 12 times faster than Image.GetPropertyItem, and around 17 times faster than the WPF JpegBitmapDecoder/BitmapMetadata API.
There's a tonne of extra information available from the library such as camera settings (F-stop, ISO, shutter speed, flash mode, focal length, ...), image properties (dimensions, pixel configurations) and other things such as GPS positions, keywords, copyright info, etc.
If you're only interested in the metadata, then using this library is very fast as it doesn't decode the image (i.e. bitmap). You can scan thousands of images in a few seconds if you have fast enough storage.
ImageMetadataReader understands many files types (JPEG, PNG, GIF, BMP, TIFF, PCX, WebP, ICO, ...). If you know that you're dealing with JPEG, and you only want Exif data, then you can make the process even faster with the following:
var directories = JpegMetadataReader.ReadMetadata(stream, new[] { new ExifReader() });
The metadata-extractor library is available via NuGet and the code's on GitHub. Thanks to all the amazing contributors who have improved the library and submitted test images over the years.
Image myImage = Image.FromFile(#"C:\temp\IMG_0325.JPG");
PropertyItem propItem = myImage.GetPropertyItem(306);
DateTime dtaken;
//Convert date taken metadata to a DateTime object
string sdate = Encoding.UTF8.GetString(propItem.Value).Trim();
string secondhalf = sdate.Substring(sdate.IndexOf(" "), (sdate.Length - sdate.IndexOf(" ")));
string firsthalf = sdate.Substring(0, 10);
firsthalf = firsthalf.Replace(":", "-");
sdate = firsthalf + secondhalf;
dtaken = DateTime.Parse(sdate);
With WPF and C# you can get the Date Taken property using the BitmapMetadata class:
MSDN - BitmapMetada
WPF and BitmapMetadata
In windows XP "FileInfo.LastWriteTime"
will return the date a picture is
taken - regardless of how many times
the file is moved around in the
filesystem.
I have great doubts XP was actually doing that. More likely the tool you used to copy the image from the camera to you hard disk was reseting the File Modified Date to the image's Date Taken.
you'll have to check the EXIF information from the picture. I don't think with regular .Net functions you'll know when the picture was taken.
It might get a little complicated...
There will be EXIF data embedded in the image. There are a ton of examples on the web if you search for EXIF and C#.
//retrieves the datetime WITHOUT loading the whole image
public static DateTime GetDateTakenFromImage(string path)
{
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
using (Image myImage = Image.FromStream(fs, false, false))
{
PropertyItem propItem = null;
try
{
propItem = myImage.GetPropertyItem(36867);
}
catch { }
if (propItem != null)
{
string dateTaken = r.Replace(Encoding.UTF8.GetString(propItem.Value), "-", 2);
return DateTime.Parse(dateTaken);
}
else
return new FileInfo(path).LastWriteTime;
}
}